I am having some issue to make work 2 auth provider at the same time for servicestack.
I am using the : JWT Tokens - Allowing users to authenticate with JWT Tokens I am my users get authenticate fine.
Still Now I would like to use the API Keys - Allowing users to authenticate with API Keys for a few external 3rd Parties user access.
Still when I Configure both my users allready authenticate by JWT Tokens doesnt work anymore.
Here is my configuration AuthProvider configuration :
IAuthProvider[] providers = new IAuthProvider[]
{
new JwtAuthProviderReader(this.AppSettings)
{
HashAlgorithm = "RS256",
PrivateKeyXml = this.AppSettings.GetString("TokenPrivateKeyXml"),
PublicKeyXml = this.AppSettings.GetString("TokenPublicKeyXml"),
RequireSecureConnection = this.AppSettings.Get<bool>("TokenUseHttps"),
EncryptPayload = this.AppSettings.Get<bool>("TokenEncryptPayload"),
PopulateSessionFilter = (session, obj, req) =>
{
ApplicationUserSession customSession = session as ApplicationUserSession;
if (customSession != null)
{
customSession.TimeZoneName = obj["TimeZoneName"];
customSession.Type = (FbEnums.UserType) (obj["UserType"].ToInt());
if (Guid.TryParse(obj["RefIdGuid"], out Guid result))
{
customSession.RefIdGuid = result;
}
}
},
},
new ApiKeyAuthProvider(AppSettings)
{
RequireSecureConnection = false
}
};
I am genereting fine the token with JwtAuth. Still It look like servicestack is not accepting my token as a valid session, because now whenever I do :
var session = httpReq.GetSession();
session.IsAuthenticated --> is always FALSE
If my remove ApiKeyAuthProvider from the configuration, token from JwtAuth working fine again.
How do I make both provider works together and tell servicestack tham some users will use JwtAuth and others ApiKeyAuth ?
You need to call a Service that requires Authentication, e.g. has the [Authenticate] attribute in order to trigger pre-Authentication for the IAuthWithRequest providers like JWT and API Key AuthProviders.
Related
As a follow-up on my question about how to setup a ROPC Flow. I want to access my API through the ROPC flow (currently using default user flows) and also through my web app which uses a custom policy on sign-in. This results in two different access tokens. On the left is access token received using the AcquireTokenSilent call and on the right is the access token received through postman with ROPC.
The custom policy token (on the left) gives an "Authorization has been denied for this request." error, while the token on the right is fine. I am assuming that the custom policy token does not work because it does not contain the tfp claim (and if it did, it would be a different one).
How can I set it up so that I can still use the ROPC flow while also using the custom policy? I would like to keep the current userjourney in the custom policy the same. Although if it is possible to somehow add ROPC as an option to it, then it would be fine.
Based on the description above, you are using two policy types - a user flow and a custom policy. And, you are attempting to get SSO between the two.
This is not a supported scenario. This is because the token uses different keys that signs the token.
If custom policies are required for your scenario, I suggest converting the user flow ROPC to a custom policy using this document https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-ropc-policy?tabs=app-reg-ga&pivots=b2c-custom-policy
So I finally found a way to do this in .NET Framework, if you want a solution for .NET Core you would sadly have to look somewhere else.
In your startup add the following.
/*
* Configure the authorization OWIN middleware
*/
private void ConfigureAuthenticationAzure(IAppBuilder app)
{
app.UseOAuthBearerAuthentication(CreateOptions(ClientId, SignUpSignInPolicy, azureDiscoveryEndpoint));
app.UseOAuthBearerAuthentication(CreateOptions(ClientId, ApiPolicy, azureDiscoveryEndpointAPI));
}
private OAuthBearerAuthenticationOptions CreateOptions(string audience, string policy, string discoveryEndpoint)
{
var metadataEndpoint = String.Format(discoveryEndpoint, Tenant, policy);
// This is the default check, in OnValidateIdentity, we check for more.
TokenValidationParameters tvps = new TokenValidationParameters
{
// This is where you specify that your API only accepts tokens from its own clients
ValidAudience = ClientId,
ValidateAudience = true,
AuthenticationType = policy,
NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier",
ValidateIssuer = true,
};
return new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint)),
Provider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = async context =>
{
try
{
var authorizationHeader = context.Request.Headers.Get("Authorization");
var userJwtToken = authorizationHeader.Substring("Bearer ".Length).Trim();
var ticket = context.Ticket;
//var identity = ticket.Identity;
var jwtSecurityToken = new JwtSecurityToken(userJwtToken);
var expiration = jwtSecurityToken.ValidTo.ToLocalTime();
if (expiration < DateTime.Now)
{
log.Warn("The JWT token has expired.");
context.Rejected();
return;
}
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(discoveryEndpoint, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdconfig = configManager.GetConfigurationAsync().Result;
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = openIdconfig.SigningKeys,
ValidateIssuer = true,
ValidIssuer = $"{AzureIssuer.ToLower()}/v2.0/",
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true,
//ClockSkew = TimeSpan.Zero
};
var handler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = handler.ValidateToken(userJwtToken, validationParameters, out securityToken);
var policyName = principal.FindFirst("tfp")?.Value;
// Add the name claim type for this authentication type
if (policyName.ToLower() == DefaultPolicy.ToLower()) // Sign In Only policy...
{
// Run specific code here for the policy that just sent a token back to the application...
context.Validated(ticket);
return;
}
else if (policyName.ToLower() == SignUpSignInPolicy.ToLower())
{
context.Validated(ticket);
return;
}
context.Rejected();
return;
}
catch(Exception ex)
{
context.Rejected();
return;
}
}
}
};
}
In my web project i want to enable the user to login with username / password and Microsoft Account.
Tech - Stack:
Asp.Net Core WebApi
Angular
Azure App Service
First i created the username / password login. Like this:
StartUp.cs:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["JWTKey"].ToString())),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
});
Login Method:
public async Task<IActionResult> ClassicAuth(AuthRequest authRequest)
{
tbl_Person person = await _standardRepository.Login(authRequest.Username, authRequest.Password);
if (person != null)
{
var claims = new[]
{
new Claim(ClaimTypes.GivenName, person.PER_T_Firstname),
};
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_config["JWTKey"].ToString()));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddHours(24),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return Ok(tokenHandler.WriteToken(token));
}
else
return Unauthorized("Invalid login data");
}
And secured my api enpoints with [Authorize].So far so good...that works.
Now i want to add a login method with Microsoft Account. I use Azure App Service Authentication / Authorization for that (https://learn.microsoft.com/de-de/azure/app-service/overview-authentication-authorization).
I configured the auth provider and i'm able to start the auth flow with a custom link in my angular app:
Login with Microsoft - Account
This works and i can retrieve the access token from my angular app with this:
this.httpClient.get("https://mysite.azurewebsites.net/.auth/me").subscribe(res => {
console.log(res[0].access_token);
});
Now the problem:
access_token seems not a valid JWT Token. If i copy the token and go to https://jwt.io/ it is invalid.
When i pass the token to my API i get a 401 - Response. With seems logical because my API checks if the JWT Token is signed with my custom JWT Key and not the Key from Microsoft.
How can I make both login methods work together? I may have some basic understanding problems at the moment.
It seems you want your Angular app calling an ASP.NET Core Web API secured with Azure Active Directory, here is a sample works well for that.
The most important step is register the app in AAD.
By the way, if you want to enable users to login one project with multiple ways in azure, you can use multiple sign-in providers.
I'm trying to use a local login form to authenticate a user credentials against its external provider (Azure Active Directory).
I understand that, per client, you can enable local login. That helps, as when set to true, I'll get the local login form but but I'm still unclear as to how to fire off the middle ware for that external provider. Is there a way to send client credentials to the external provider to receive an ID token? My current code redirects to the Microsoft login; and then back to my identity server, and then the client application. I want the user to login in through identity server but not have them know it's really authenticating against Azure.
Here's my start up:
var schemeName = "Azure-AD";
var dataProtectionProvibder = app.ApplicationServices.GetRequiredService<IDataProtectionProvider>();
var distributedCache = app.ApplicationServices.GetRequiredService<IDistributedCache>();
var dataProtector = dataProtectionProvider.CreateProtector(
typeof(OpenIdConnectMiddleware).FullName,
typeof(string).FullName, schemeName,
"v1");
var dataFormat = new CachedPropertiesDataFormat(distributedCache, dataProtector);
///
/// Azure AD Configuration
///
var clientId = Configuration["AzureActiveDirectory:ClientId"];
var tenantId = Configuration["AzureActiveDirectory:TenantId"];
Redirect = Configuration["AzureActiveDirectory:TenantId"];
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = schemeName,
DisplayName = "Azure-AD",
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
ClientId = clientId,
Authority = $"https://login.microsoftonline.com/{tenantId}",
ResponseType = OpenIdConnectResponseType.IdToken,
StateDataFormat = dataFormat,
});
app.UseIdentity();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
This is the login.
[HttpGet]
public async Task<IActionResult> ExternalLogin(string provider, string returnUrl)
{
var context = this.HttpContext.Authentication;
List<AuthenticationDescription> schemes = context.GetAuthenticationSchemes().ToList();
returnUrl = Url.Action("ExternalLoginCallback", new { returnUrl = returnUrl });
// start challenge and roundtrip the return URL
var props = new AuthenticationProperties
{
RedirectUri = returnUrl,
Items = { { "scheme", provider } }
};
//await HttpContext.Authentication.ChallengeAsync(provider, props);
return new ChallengeResult(provider, props);
}
In my opinion ,we shouldn't directly pass the username/password directly from other Idp to azure AD for authentication as a security implementation .And even Azure AD supports the Resource Owner Password Credentials Grant ,it's only available in native client. I suggest you keep the normal way and don't mix them .
Our Mvc/WebAPI solution currently has four trusted identity providers which we have registered in ADFS3. Each of these identity providers can be used by our users by direct links, effectively working around any home-realm-cookies that ADFS may have created (eg: www.ourportal.com/accounts/facebook or www.ourportal.com/accounts/twitter). Currently we are migrating from WIF to OWIN but will keep using WS-Federation protocol for the time being by implementing wsfederation and cookie authentication middleware. When using WIF, we did the following in order to go directly to a known identity provider:
var signInRequest = new SignInRequestMessage(stsUrl, realm) { HomeRealm = homeRealm };
return new RedirectResult(signInRequest.WriteQueryString());
This seems to have two concerning behaviors, it does not pass the WsFedOwinState parameter, and on the return back to the Relying Party, the Home.cshtml is built (with a windows principal) before the the Owin authentication middleware is fired. The Home.cshtml being fired before the Owin middleware is the most concering as this view relies on Claims that would is provided in the transformation done by the authentication pipeline, which is fired afterwards and thus our view does not work. It works in the correct order when going to the portal in the normal way (eg www.ourportal.com)
I understand that in order to provide the Whr parameter, you do the following when configuring the ws-federation middleware:
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.Whr = "SomeUrnOfAnIdentityProvider";
return Task.FromResult(0);
}
but this sets a single identity provider for the whole solution and does not allow our users to go directly to one of a list of identity providers.
The non-working method which builds the sign-in-request is currently:
private RedirectResult FederatedSignInWithHomeRealm(string homeRealm)
{
var stsUrl = new Uri(ConfigurationManager.AppSettings["ida:Issuer"]);
string realm = ConfigurationManager.AppSettings["ida:Audience"];
var signInRequest = new SignInRequestMessage(stsUrl, realm)
{
HomeRealm = homeRealm
};
HttpContext.Request.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return new RedirectResult(signInRequest.WriteQueryString());
}
The ws-federation and cookie middleware are configured as the first middleware in OWIN startup and the default authentication is set to
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
I think I found a solution. The new method for skipping the home realm screen would be like this :
private void FederatedSignInWithHomeRealm(string homeRealm)
{
HttpContext.Request
.GetOwinContext()
.Authentication
.SignOut(CookieAuthenticationDefaults.AuthenticationType);
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
authenticationProperties.Dictionary.Add("DirectlyToIdentityProvider", homeRealm);
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
}
And the OWIN WS-Federation middleware would be configured like this :
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions
{
Notifications = new WsFederationAuthenticationNotifications()
{
RedirectToIdentityProvider = notification =>
{
string homeRealmId = null;
var authenticationResponseChallenge = notification.OwinContext
.Authentication
.AuthenticationResponseChallenge;
var setIdentityProvider = authenticationResponseChallenge != null
&& authenticationResponseChallenge.Properties
.Dictionary
.TryGetValue("DirectlyToIdentityProvider", out homeRealmId);
if (setIdentityProvider)
{
notification.ProtocolMessage.Whr = homeRealmId;
}
return Task.FromResult(0);
}
},
MetadataAddress = wsFedMetadata,
Wtrealm = realm,
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = realm
}
});
I am doing external login (Facebook, Twitter, Microsoft) using MVC 5 OWIN Identity 2, which works great, but I need to access a mobile services with this credential, I have read that to this I need a access token, so I get the access token and try to pass it to the mobile services, but always has this error:
Facebook: Error:
The Facebook Graph API access token authorization request failed with HTTP status code 400
Microsoft: Error:
Invalid token format. Expected Envelope.Claims.Signature.
The method that I am trying to use with mobile services is:
await mobileservi.LoginAsync(MobileServiceAuthenticationProvider.[ProviderName], token);
I read on this link:
http://msdn.microsoft.com/en-us/library/dn296411.aspx
So I am using a JObject() to pass the access token
The format of the token that I most pass:
For Microsoft is:
token.Add("authenticationToken", _accessToken);
{"authenticationToken":"<authentication_token>"}
For Facebook is:
token.Add("access_token", _accessToken);
{"access_token":"<access_token>"}
But I do not have the format for Twitter.
Now according to Azure Mobile Services documentation, I most use the azure mobile services URL on my apps for any of this providers, but if I do this, I receive an error of incorrect URL when redirecting to the provider log in page.
I read this post with OAuth:
http://blogs.msdn.com/b/carlosfigueira/archive/2013/06/25/exposing-authenticated-data-from-azure-mobile-services-via-an-asp-net-mvc-application.aspx
It has to be something like this for MVC 5 OWIN Identity 2.
On the Startuo.Auth.cs file, I have this configure to get the access token for each provider:
Microsoft:
var MicrosoftOption = new MicrosoftAccountAuthenticationOptions()
{
ClientId = "0000000048124A22",
ClientSecret = "c-gTye48WE2ozcfN-bFMVlL3y3bVY8g0",
Provider = new MicrosoftAccountAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new Claim(("urn:microsoftaccount:access_token", context.AccessToken, XmlSchemaString, "Microsoft"));
return Task.FromResult(0);
}
}
};
Twitter:
var twitterOption = new TwitterAuthenticationOptions()
{
ConsumerKey = "ConsumerKey",
ConsumerSecret = "ConsumerSecret",
Provider = new TwitterAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new Claim("urn:tokens:twitter:accesstoken", context.AccessToken));
context.Identity.AddClaim(new Claim("urn:tokens:twitter:accesstokensecret", context.AccessTokenSecret));
return Task.FromResult(0);
}
}
};
Facebook:
var facebookOption = new FacebookAuthenticationOptions()
{
AppId = "AppId",
AppSecret = "AppSecret",
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook"));
return Task.FromResult(0);
}
}
};
On the externalLoginCallback, this is how a retrieve the access token
string email = null;
string accessToken = null;
ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
switch (login.LoginProvider)
{
case "Facebook":
accessToken = ext.Claims.First(x => x.Type.Contains("access_token")).Value;
break;
case "Twitter":
accessToken = ext.Claims.First(x => x.Type.Contains("accesstoken")).Value;
break;
case "Microsoft":
accessToken = ext.Claims.First(x => x.Type.Contains("access_token")).Value;
break;
}
Later I store this value on a session variable, this value is the one that I use to pass as the access token.
So I have no idea what to do, can anyone please help me?
OK, I found what I was doing wrong, in order to respect the authorization flow, I must have APP ID and APP Secret that I register on my app (Google, Facebook, Microsoft, Twitter), on my mobile service. This is the important part, the register URL in the app must be the URL of the web site, after doing this, everything work fine