I want to get my Azure B2C AD access token from the httpContext or similar, but I am using .NET framework 4.7.2. If I was using .Net core, I would use HttpContext.Authentication.GetTokenAsync() .
Background
I am using OpenIdConnect 4.1.0.
My OpenIdConnectAuthenticationOptions looks like this:
private OpenIdConnectAuthenticationOptions CreateOptionsFromSiteConfig(B2CConfig config)
{
OpenIdConnectAuthenticationOptions options = new OpenIdConnectAuthenticationOptions();
options.MetadataAddress = string.Format(_aadInstance, _tenant, config.Policy);
options.AuthenticationType = [B2CAD-POLICY-name];
options.AuthenticationMode = AuthenticationMode.Passive;
options.RedirectUri = config.AzureReplyUri;
options.PostLogoutRedirectUri = config.LogoutRedirectUri;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "emails"
};
options.SaveTokens = true;
options.RedeemCode = true;
var identityProvider = GetIdentityProvider();
options.Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = AuthenticationFailed,
RedirectToIdentityProvider = notification =>
{
return Task.FromResult(notification.ProtocolMessage.UiLocales = config.UiLocale ?? string.Empty);
},
SecurityTokenValidated = notification =>
{
notification.AuthenticationTicket.Identity.AddClaim(new Claim("idp", "azureadb2c"));
// transform all claims
ClaimsIdentity identity = notification.AuthenticationTicket.Identity;
notification.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
return Task.CompletedTask;
},
};
options.ClientId = config.ClientId;
options.Scope = "openid [api-scope-here]";
options.ResponseType = "id_token token";
return options;
}
There is multiple policies with the same name (i.e. multiple AuthenticationTypes with same name).
So far, I have found several suggestions, where the most promising one suggested:
var result = await owinContext.Authentication.AuthenticateAsync([B2CAD-POLICY-name]));
string token = result.Properties.Dictionary["access_token"];
However, the result is always NULL, eventhough I have verified that the b2cad policy is actually present in the OwinContext.
Any help is much appreciated!
Related
I am just learning Auth0 and I am using the sample Asp.Net MVC app from here.
I note that the ClaimsIdentity can access certain user information such as profile and email by defining access to scopes in the Auth0 configuration in startup.cs as follows:
// Configure Auth0 authentication
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "Auth0",
Authority = $"https://{auth0Domain}",
ClientId = auth0ClientId,
RedirectUri = auth0RedirectUri,
PostLogoutRedirectUri = auth0PostLogoutRedirectUri,
// This is where the Scopes are defined
Scope = "openid profile email",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "https://schemas.quickstarts.com/roles"
},
// More information on why the CookieManager needs to be set can be found here:
// https://learn.microsoft.com/en-us/aspnet/samesite/owin-samesite
CookieManager = new SameSiteCookieManager(new SystemWebCookieManager()),
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = notification =>
{
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var logoutUri = $"https://{auth0Domain}/v2/logout?client_id={auth0ClientId}";
var postLogoutUri = notification.ProtocolMessage.PostLogoutRedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = notification.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
}
notification.Response.Redirect(logoutUri);
notification.HandleResponse();
}
return Task.FromResult(0);
}
}
What I would like to do is retrieve the username field. I assumed the username would be part of the Profile scope but it is not. I tried adding username to the Scope definition (Scope = "openid profile email username") but this didn't work.
Does anyone know how I access the username field?
Thanks
I have an asp.net core application using json web tokens for authentication, this worked fine when my user Id was a string, but stopped working after changing to an int.
My IdentityUser type was originally this
public class AppUser : IdentityUser
{
//other properties...
}
I updated this;
public class AppUser : IdentityUser<int>
{
//other properties...
}
I changed my Startup configuration from this;
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtIssuerOptions.Issuer;
options.Audience = jwtIssuerOptions.Audience;
options.SigningCredentials = jwtIssuerOptions.SigningCredentials;
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtIssuerOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtIssuerOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtIssuerOptions.Issuer;
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
// add identity
var builder = services.AddIdentityCore<AppUser>(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
o.SignIn.RequireConfirmedEmail = true;
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
To this;
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtIssuerOptions.Issuer;
options.Audience = jwtIssuerOptions.Audience;
options.SigningCredentials = jwtIssuerOptions.SigningCredentials;
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtIssuerOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtIssuerOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtIssuerOptions.Issuer;
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
// add identity
var builder = services.AddIdentity<AppUser, IdentityRole<int>>(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
o.SignIn.RequireConfirmedEmail = true;
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole<int>), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
I use a the following method to generate the token, this is unchanged between implementations.
public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
identity.FindFirst(JwtClaimIdentifiers.Rol),
identity.FindFirst(JwtClaimIdentifiers.Id)
};
// Create the JWT security token and encode it.
var jwt = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: _jwtOptions.NotBefore,
expires: _jwtOptions.Expiration,
signingCredentials: _jwtOptions.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
return encodedJwt;
}
And finally this method is used to create the claims identity;
public ClaimsIdentity GenerateClaimsIdentity(string userName, int id)
{
return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[]
{
new Claim(JwtClaimIdentifiers.Id, id.ToString(), ClaimValueTypes.Integer32),
});
}
User.Identity.IsAuthenticated is now always false when validating and the ClaimsIdentity appears to be empty of the information I saw before converting my AppUser from a string ID type to an int Id type.
The best theory I have so far is that the problem may be related to serialisation/deserialisation of the token values but I'm clutching at straws and have little idea how I might debug it.
What have I missed?
Nothing glaring at me in your code (it looks like the primary difference is you swapped from AddIdentity to AddIdentityCore?), but I find with JWT auth issues, it really helps to run the app in debug mode and add handlers for JwtBearerEvents.
Add this to your Startup .AddJwtBearer call:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// ...
// Add this at the end:
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
// Put a breakpoint here
System.Console.WriteLine(context.Exception);
return Task.CompletedTask;
},
};
})
If you run in debug, will be able to inspect the context.Exception:
This should give you a better explanation of why JWT authentication is failing.
Taking Connor Low's advice I supplied delegates to all the JwtBearerOptions events. None of these where hit, indicating that my setup was insufficient and that the authentication scheme I had configured was not being used at all.
I reverted to addIdentityCore
var builder = services.AddIdentityCore<AppUser>(o =>
Although I could no longer specify that I was using IdentityRole<int> instead of the default like this;
var builder = services.AddIdentity<AppUser, IdentityRole<int>>(o =>
This seemed to be unnecessary provided I had the following line;
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole<int>), builder.Services);
With these changes in place authentication worked and the breakpoints I had added at Connor Low's suggestion were being hit.
We are working on a MVC5 web application, that uses OpenIdConnect to authenticate to Azure AD B2C. When a user has authenticated, we would like to be able to acquire accesstokens from Azure AD B2C, in order to use them our API.
This is our Startup.cs-equivalent code:
protected override void ProcessCore(IdentityProvidersArgs args)
{
Assert.ArgumentNotNull(args, nameof(args));
List<B2CConfig> ssoSettings = _ssoConfigurationRepository.GetAllSettings();
foreach (var config in ssoSettings)
{
args.App.UseOpenIdConnectAuthentication(CreateOptionsFromSiteConfig(config));
}
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromSiteConfig(B2CConfig config)
{
OpenIdConnectAuthenticationOptions options = new OpenIdConnectAuthenticationOptions();
options.MetadataAddress = string.Format(_aadInstance, _tenant, config.Policy);
options.AuthenticationType = config.Policy;
options.AuthenticationMode = AuthenticationMode.Passive;
options.RedirectUri = config.AzureReplyUri;
options.PostLogoutRedirectUri = config.LogoutRedirectUri;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "emails"
};
var identityProvider = GetIdentityProvider();
options.Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = AuthenticationFailed,
RedirectToIdentityProvider = notification =>
{
return Task.FromResult(notification.ProtocolMessage.UiLocales = config.UiLocale ?? string.Empty);
},
SecurityTokenValidated = notification =>
{
notification.AuthenticationTicket.Identity.AddClaim(new Claim("idp", "azureadb2c"));
// transform all claims
ClaimsIdentity identity = notification.AuthenticationTicket.Identity;
notification.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
return Task.CompletedTask;
}
};
options.ClientId = config.ClientId;
options.Scope = "openid";
options.ResponseType = "id_token";
return options;
}
private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
SsoLogger.Warn("User triggered reset password");
notification.Response.Redirect(SsoConfiguration.Routes.ResetPassword);
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
SsoLogger.Warn("AuthenticationFailed", notification.Exception);
notification.Response.Redirect(SsoConfiguration.Routes.LoginError);
}
return Task.FromResult(0);
}
In Asp.Net core it seems like you would call GetTokenAsync on the HttpContext, but that extensionmethod is not available in .NET 4.72.
Can anyone help figuring out, how to retrieve an accesstoken from AzureAD B2C, that can be used in the calls to our WebApi? Or can I just store the accesstoken I get from the SecurityTokenValidated event and use that for all API requests?
This is a possible solution: Is it safe to store an access_token in a user claim for authorization?
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = notification =>
{
var identity = notification.AuthenticationTicket.Identity;
identity.AddClaim(claim: new Claim(type: "auth_token", value:
notification.ProtocolMessage.AccessToken));
return Task.CompletedTask;
}
}
If anyone has a better approach I will gladly accept another answer.
I get an error
Cannot redirect to the end session endpoint, the configuration may be
missing or invalid when signing out.
when I handle signing out
public async Task LogOut()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync("oidc");
}
Schemas
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignOutScheme = "oidc";
sharedOptions.DefaultChallengeScheme = "oidc";
})
.AddCookie(options =>
{
options.AccessDeniedPath = new PathString("/Access/Unauthorised");
options.Cookie.Name = "MyCookie";
})
.AddOpenIdConnect("oidc", options =>
{
options.ClientId = Configuration["oidc:ClientId"];
options.ClientSecret = Configuration["oidc:ClientSecret"]; // for code flow
options.SignedOutRedirectUri = Configuration["oidc:SignedOutRedirectUri"];
options.Authority = Configuration["oidc:Authority"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = new PathString("/oidc");
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("pfidpadapterid", Configuration["oidc:PingProtocolMessage"]);
return Task.FromResult(0);
}
};
});
It seems like your authorization server does not support Session Management and Dynamic Registration. When it's supported, the discovery response contains end_session_endpoint. This is not the same as SignedOutRedirectUri, which is used as a final redirection target when the user is logged out on authorization server.
OnRedirectToIdentityProviderForSignOut event provides an option to set the issuer address, which in this case is the logout URI:
options.Events = new OpenIdConnectEvents()
{
options.Events.OnRedirectToIdentityProviderForSignOut = context =>
{
context.ProtocolMessage.IssuerAddress =
GetAbsoluteUri(Configuration["oidc:EndSessionEndpoint"], Configuration["oidc:Authority"]);
return Task.CompletedTask;
};
}
The helper method is used to support both relative and absolute path in the configuration:
private string GetAbsoluteUri(string signoutUri, string authority)
{
var signOutUri = new Uri(signoutUri, UriKind.RelativeOrAbsolute);
var authorityUri = new Uri(authority, UriKind.Absolute);
var uri = signOutUri.IsAbsoluteUri ? signOutUri : new Uri(authorityUri, signOutUri);
return uri.AbsoluteUri;
}
This way the authorization server will get additional parameters in the query string, which it can use e.g. to redirect back to your application.
To fix this handle the OnRedirectToIdentityProviderForSignOut event and specify the Logout endpoint manually:
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("pfidpadapterid", Configuration["oidc:PingProtocolMessage"]);
return Task.FromResult(0);
},
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = context =>
{
var logoutUri = Configuration["oidc:SignedOutRedirectUri"];
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
I have a system where I have an MVC website calling a web api. I have used OAUTH and OPENID Connect for my authentication/authorization. I have setup an identity server in another web api project using Thinktecture's IdentityServer3. In the MVC project I am doing the redirect to the identity server in the OWIN Startup class. This is all pretty standard stuff and is working fine.
I have now been asked to put the web api and the identity server behind Azure API Management. This also, is fine as all I need to do from my MVC project is add my subscription key from API management as a header ("Ocp-Apim-Subscription-Key") to any request to the web api. So far so good.
The problem is I now need to add this header to any requests to the identity server and I can't work out how, other than writing my own middleware. My Startup class looks like this:
public class Startup
{
public void Configuration(IAppBuilder app)
{
DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(IocHelper.GetContainer()));
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationHelper.GetClientId(),
Authority = ConfigurationHelper.GetSecurityTokenServiceUrl(),
RedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
PostLogoutRedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
ResponseType = "code id_token",
Scope = "openid profile public_api",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(ConfigurationHelper.GetTokenEndpointUrl(), ConfigurationHelper.GetClientId(), ConfigurationHelper.GetClientSecret());
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(ConfigurationHelper.GetUserInfoUrl());
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
if (userInfoResponse.IsError)
{
throw new Exception(userInfoResponse.Error);
}
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
foreach (var c in userInfoResponse.Claims)
{
id.AddClaim(new Claim(c.Type, c.Value));
}
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn * 2).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
var claimsIdentity = new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
//claimsIdentity.IsAuthenticated = true;
n.AuthenticationTicket = new AuthenticationTicket(claimsIdentity, n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
{
return Task.FromResult(0);
}
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
// DOESN'T WORK
n.OwinContext.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
// ALSO DOESN'T WORK
n.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
return Task.FromResult(0);
}
}
});
}
}
Is there a way to hi-jack the request to the identity server and add my own header in there?