OpenIddict Samples - Add 'unique-name' to the id_token - asp.net-core

I am playing around with the RefreshFlow sample of OpenIddict-Samples. It works great. I notice in the Angular models there is a ProfileModel that is populated from the JWT_Decode of the id_token:
export interface ProfileModel {
sub: string;
jti: string;
useage: string;
at_hash: string;
nbf: number;
exp: number;
iat: number;
iss: string;
unique_name: string;
email_confirmed: boolean;
role: string[];
}
I can't see where on the server the unique_name is being populated. I have a requirement for this field and tried applying the value here:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange([ModelBinder(typeof(OpenIddictMvcBinder))] OpenIdConnectRequest request)
{
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Validate the username/password parameters and ensure the account is not locked out.
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (!result.Succeeded)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
{ "unique_name", "hello World!" }
});
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user, properties);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
Is this where I need to add it? I previously rolled my own token creator using JwtSecureDataFormat : ISecureDataFormat and added the field as a property.
How can I add it with OpenIddict/ASOS?
Thanks!

So I figured out how to achieve mostly what I wanted!!
I really didn't need to specifically add 'unique_name' to the token but simply add more claims than what the standard Identity framework adds for you.
This is how I did it:
Create a custom SignInManager:
public class OpenIdictSignInManager<TUser> : SignInManager<TUser> where TUser : IdentityUser
{
public OpenIdictSignInManager(
UserManager<TUser> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<TUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<TUser>> logger,
IAuthenticationSchemeProvider schemes) : base(userManager,
contextAccessor,
claimsFactory,
optionsAccessor,
logger,
schemes)
{
}
public override async Task<ClaimsPrincipal> CreateUserPrincipalAsync(TUser user)
{
var principal = await base.CreateUserPrincipalAsync(user);
var identity = (ClaimsIdentity)principal.Identity;
identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.EmailVerified, user.EmailConfirmed.ToString().ToLower()));
return principal;
}
}
Then applied the new SignInManager to the startup.cs configuration:
// Register the Identity services.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddSignInManager<OpenIdictSignInManager<ApplicationUser>>();
Then added a claim destination when creating the ticket in AuthorizationController:
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
continue;
}
var destinations = new List<string>();
// Identity Token destinations only
if (new List<string>
{
OpenIdConnectConstants.Claims.EmailVerified
}.Contains(claim.Type))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
claim.SetDestinations(destinations);
continue;
}
destinations.Add(OpenIdConnectConstants.Destinations.AccessToken);
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
(claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
(claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(destinations);
}
It took me a few days of digging through code and googling to come up with this approach so I thought I'd share and hope it helps someone else out :)

Related

How to migrate .NET Framework 4.x ASP.NET Web API OWIN `client_credentials` OAuth Bearer Token implementation to .NET 6

Problem
I have an existing .NET Framework 4.x Web API that uses OWIN to handle token requests, create and issue tokens and validate them.
The Web API project is being migrated to .NET 6.
As far as I'm aware .NET 6 does not include any functionality that can create tokens, so a third party solution is required. See https://developer.okta.com/blog/2018/03/23/token-authentication-aspnetcore-complete-guide#generate-tokens-for-authentication-in-aspnet-core for more details.
I need a code solution to this problem not an OAuth cloud provider solution.
Possible solution 1
Firstly I looked into IdentityServer 6 from Duende but the licensing costs were prohibitive, even though we have a single Web API to secure we allow each of our clients to generate up to 5 unique ClientId/ClientSecret combinations for their use to access the API.
We currently have around 200+ unique ClientId/ClientSecret combinations that can access our API.
To support this number of clients we would have needed to purchase the Enterprise edition of IdentityServer 6 which is currently $12,000 USD per year.
Possible solution 2
One of my colleagues' suggested using OpenIddict 3.0
From colleague:
"I found AspNet.Security.OpenIdConnect.Server which appears similar to how we currently provide OAuth, it's been merged into OpenIddict 3.0. Apparently DegradedMode allows us to do our own token validation like in the current OWIN provider."
I have created a POC prototype using OpenIddict 3.0 with DegradedMode enabled.
My concern is that because I've enabled DegradedMode that its not down to me to ensure that I've done everything right as I've disabled all of the out of the box goodness.
My question is, looking at the existing implementation details below, do I need to enable DegradedMode in OpenIddict 3.0 in order to have the same functionality as OWIN gives us?
Current .NET Framework 4.x Web API using OWIN implementation details
****************************************
Request New Token
****************************************
ApplicationOAuthProvider.cs => ValidateClientAuthentication(...)
=> GrantClientCredentials(...)
=> GetClaimsIdentities(...)
=> CreateProperties(...)
=> TokenEndpoint(...)
AccessTokenProvider.cs => Create(...)
RefreshTokenProvider.cs => Create(...) (no-ops as AllowRefresh is false)
****************************************
Send API request with Bearer token
****************************************
MyAppOAuthBearerAuthenticationProvider.cs => RequestToken(...)
AccessTokenProvider.cs => Receive...)
Web API
Startup.Auth.cs
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
// Configure the application for OAuth based flow
var oauthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/oauth/token"),
Provider = new ApplicationOAuthProvider(),
AuthorizeEndpointPath = new PathString("/oauth/authorize"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true,
AccessTokenProvider = new AccessTokenProvider(),
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthAuthorizationServer(oauthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new MyAppOAuthBearerAuthenticationProvider("/oauth/"),
AccessTokenProvider = new AccessTokenProvider()
});
}
}
ApplicationOAuthProvider.cs
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// grant_type=client_credentials
/// </summary>
public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
var partner = context.OwinContext.Get<Partner>(OwinKeys.Partner);
Account account = null;
var accountScopes = context.Scope.Where(s => s.StartsWith("account:")).ToList();
if (accountScopes.Count > 1)
{
// Tokens cannot be scoped to multiple accounts.
context.Rejected();
context.SetError("invalid_grant", "Only one account scope can be provided");
return;
}
if (accountScopes.Count == 1)
{
var accountId = accountScopes[0].Substring("account:".Length);
account = await DependencyResolver.Current.GetService<IAccountService>().FindAsync(partner.Id, accountId);
if (account?.Status != AccountStatus.Active)
{
context.Rejected();
context.SetError("invalid_scope", "Account not found.");
return;
}
context.OwinContext.Set(OwinKeys.Account, account);
}
var (oAuthIdentity, cookiesIdentity) = await GetClaimIdentities(partner, account, null, null).ConfigureAwait(false);
var properties = CreateProperties(context.ClientId, null);
var ticket = new AuthenticationTicket(oAuthIdentity, properties);
// Disable refresh token for client_credentials.
// 'A refresh token SHOULD NOT be included.' https://tools.ietf.org/html/rfc6749#section-4.4.3
properties.AllowRefresh = false;
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(properties, cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (var property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
/// <summary>
/// Validate that the request is using valid OAuth ClientId.
/// </summary>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret) && !context.TryGetFormCredentials(out clientId, out clientSecret))
{
context.Rejected();
context.SetError("invalid_client", "Client credentials could not be retrieved through the Authorization header.");
return;
}
var partnerService = DependencyResolver.Current.GetService<IPartnerService>();
var partnerOAuthClient = await partnerService.GetPartnerOAuthClientByClientIdAsync(clientId).ConfigureAwait(false);
if (partnerOAuthClient == null || (partnerOAuthClient.ClientSecret != null && partnerOAuthClient.ClientSecret != clientSecret))
{
// Client could not be validated.
context.Rejected();
context.SetError("invalid_client", "Client credentials are invalid.");
return;
}
context.OwinContext.Set(OwinKeys.Partner, partnerOAuthClient.Partner);
// Client has been verified.
context.Validated(clientId);
}
private AuthenticationProperties CreateProperties(string clientId, string username)
{
var data = new Dictionary<string, string>
{
{ "clientId", clientId }
};
if (!string.IsNullOrWhiteSpace(username))
{
data.Add("userName", username);
}
return new AuthenticationProperties(data);
}
/// <summary>
/// Gets the OAuth and Cookie claims identities.
/// </summary>
/// <param name="partner">Partner the token is for.</param>
/// <param name="account">Account the token is for, null if not account restricted token.</param>
/// <param name="user">User the token is for, if using password grant.</param>
/// <param name="userManager">ApplicationUserManager to generate ClaimsIdentity for user, only required for password grant.</param>
private Tuple<ClaimsIdentity, ClaimsIdentity> GetClaimIdentities(Partner partner, Account account)
{
ClaimsIdentity oAuthIdentity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationType);
oAuthIdentity.AddClaim(new Claim("http://myapp.com/claims/partnerid", partner.Id.ToString(), ClaimValueTypes.Integer));
cookiesIdentity.AddClaim(new Claim("http://myapp.com/claims/partnerid", partner.Id.ToString(), ClaimValueTypes.Integer));
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, IdentityRoleNames.User));
cookiesIdentity.AddClaim(new Claim(ClaimTypes.Role, IdentityRoleNames.User));
if (account == null)
{
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, "Partner"));
cookiesIdentity.AddClaim(new Claim(ClaimTypes.Role, "Partner"));
}
else
{
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, "Account"));
cookiesIdentity.AddClaim(new Claim(ClaimTypes.Role, "Account"));
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Sid, account.Id.ToString()));
cookiesIdentity.AddClaim(new Claim(ClaimTypes.Sid, account.Id.ToString()));
}
return Tuple.Create(oAuthIdentity, cookiesIdentity);
}
}
AccessTokenProvider.cs
public class AccessTokenProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
Guid? accountId = null;
var accessTokenService = DependencyResolver.Current.GetService<IAccessTokenService>();
AccessTokenScope scope = AccessTokenScope.None;
if (context.Ticket.Identity.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Account"))
{
scope = AccessTokenScope.Account;
accountId = Guid.Parse(context.Ticket.Identity.Claims.First(c => c.Type == ClaimTypes.Sid).Value);
}
else if (context.Ticket.Identity.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Partner"))
scope = AccessTokenScope.Partner;
if (scope == AccessTokenScope.None)
throw new ArgumentNullException(nameof(AccessTokenScope));
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
var accessTokenHash = GetTokenHash(context.Token);
context.OwinContext.Set(OwinKeys.OAuthAccessTokenHash, accessTokenHash);
accessTokenService.Insert(new AccessToken
{
TokenHash = accessTokenHash,
Ticket = context.SerializeTicket(),
ExpiresUtc = context.Ticket.Properties.ExpiresUtc.Value.UtcDateTime,
ClientId = context.Ticket.Properties.Dictionary["clientId"],
UserId = context.Ticket.Identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value,
PartnerId = int.Parse(context.Ticket.Identity.Claims.First(c => c.Type == "http://myapp.com/claims/partnerid").Value),
Scope = scope,
AccountId = accountId
});
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
var accessTokenService = DependencyResolver.Current.GetService<IAccessTokenService>();
var accessToken = accessTokenService.Find(GetTokenHash(context.Token));
if (accessToken != null)
context.DeserializeTicket(accessToken.Ticket);
}
public static string GetTokenHash(string token)
{
var sha = new SHA256Managed();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(token));
for (var i = 0; i < 9999; i++)
hash = sha.ComputeHash(hash);
return Convert.ToBase64String(hash);
}
}
RefreshTokenProvider.cs
public class RefreshTokenProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
if (context.Ticket.Properties.AllowRefresh == false)
return;
var refreshTokenService = DependencyResolver.Current.GetService<IRefreshTokenService>();
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
string key = context.Token;
context.Ticket.Properties.ExpiresUtc = DateTimeOffset.MaxValue;
string value = context.SerializeTicket();
RefreshToken refreshToken = new RefreshToken
{
Key = key,
Value = value,
AccessTokenHash = context.OwinContext.Get<string>(OwinKeys.OAuthAccessTokenHash)
};
refreshTokenService.InsertRefreshToken(refreshToken);
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
var refreshTokenService = DependencyResolver.Current.GetService<IRefreshTokenService>();
var accessTokenService = DependencyResolver.Current.GetService<IAccessTokenService>();
var refreshToken = refreshTokenService.GetRefreshTokenByKey(context.Token);
if (refreshToken != null)
{
context.DeserializeTicket(refreshToken.Value);
accessTokenService.Delete(refreshToken.AccessTokenHash);
refreshTokenService.DeleteRefreshToken(refreshToken);
}
}
}
MyAppOAuthBearerAuthenticationProvider.cs
public class MyAppOAuthBearerAuthenticationProvider : OAuthBearerAuthenticationProvider
{
private readonly string _oauthRequestPath;
public MyAppOAuthBearerAuthenticationProvider(string oauthRequestPath)
{
_oauthRequestPath = oauthRequestPath;
}
public override async Task RequestToken(OAuthRequestTokenContext context)
{
if (context.Token != null && !context.Request.Path.Value.StartsWith(_oauthRequestPath))
{
// Need to check the token is still valid.
var accessTokenService = DependencyResolver.Current.GetService<IAccessTokenService>();
var accessToken = accessTokenService.Find(AccessTokenProvider.GetTokenHash(context.Token));
if (accessToken == null)
{
context.Token = null;
return;
}
// Check for expired token.
if (accessToken.ExpiresUtc < DateTime.UtcNow)
{
context.Token = null;
accessTokenService.Delete(accessToken);
return;
}
var revokeToken = false;
Partner partner = null;
string externalAccountId = null;
Guid? accountId = null;
if (accessToken.Scope == AccessTokenScope.Partner && context.Request.Headers.ContainsKey(MyAppWebParameters.APIAccountHeaderName))
{
if (string.IsNullOrWhiteSpace(context.Request.Headers[MyAppWebParameters.APIAccountHeaderName]))
{
context.Token = null;
return;
}
externalAccountId = context.Request.Headers[MyAppWebParameters.APIAccountHeaderName]; // Set the account ID from the header.
}
else if (accessToken.Scope == AccessTokenScope.Account)
{
accountId = accessToken.AccountId; // Set the account ID from the token.
}
var scope = externalAccountId != null || accountId != null ? AccessTokenScope.Account : accessToken.Scope;
switch (scope)
{
case AccessTokenScope.Account:
// Check the account still exists.
var accountService = DependencyResolver.Current.GetService<IAccountService>();
var account = externalAccountId != null ? await accountService.FindAsync(accessToken.PartnerId, externalAccountId) :
accountId != null ? await accountService.FindAsync(accessToken.PartnerId, accountId.Value) : null;
if (account?.Status == AccountStatus.DeleteScheduled)
{
// Account is scheduled to be deleted, don't want to revoke the token yet incase delete was mistake and cancelled.
context.Token = null;
return;
}
partner = account?.Partner;
revokeToken = account == null || account.Partner?.OAuthClients?.Any(s => s.Id == accessToken.ClientId) != true ||
account.Status != AccountStatus.Active || account.Partner?.Id != accessToken.PartnerId;
if (revokeToken && accessToken.Scope == AccessTokenScope.Partner)
{
// Don't revoke partner tokens if account not found or for different partner.
context.Token = null;
return;
}
if (!revokeToken)
{
context.OwinContext.Set(OwinKeys.Account, account);
context.OwinContext.Set(OwinKeys.Partner, account.Partner);
}
break;
case AccessTokenScope.Partner:
// Check that the partner client id hasn't changed.
var partnerService = DependencyResolver.Current.GetService<IPartnerService>();
partner = (await partnerService.GetPartnerOAuthClientByClientIdAsync(accessToken.ClientId))?.Partner;
revokeToken = partner?.Id != accessToken.PartnerId;
if (!revokeToken)
context.OwinContext.Set(OwinKeys.Partner, partner);
break;
case AccessTokenScope.None:
default:
break;
}
if (partner?.PartnerStatus != PartnerStatus.Active || partner?.TrialExpired == true)
throw new UnauthorizedAccessException();
if (revokeToken)
{
context.Token = null;
accessTokenService.Delete(accessToken);
return;
}
}
await base.RequestToken(context);
}
}
Current POC prototype OpenIddict implementation details
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
var migrationsAssembly = typeof(Program).Assembly.GetName().Name;
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<OAuthDbContext>(options =>
{
// Configure the context to use Microsoft SQL Server.
options.UseMySql(connectionString,
new MySqlServerVersion(new Version(CyclrParameters.MySqlMajorVersion,
CyclrParameters.MySqlMinorVersion,
CyclrParameters.MySqlBuildVersion)),
mySql => mySql.MigrationsAssembly(migrationsAssembly));
// Register the entity sets needed by OpenIddict but use the specified entities instead of the default ones.
options.UseOpenIddict<OAuthApplication, OAuthAuthorization, OAuthScope, OAuthToken, string>();
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthentication("cyclr")
.AddCookie();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationPolicies.UserPolicy, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole(AuthorizationRoles.UserRole);
});
options.AddPolicy(AuthorizationPolicies.PartnerPolicy, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole(AuthorizationRoles.PartnerRole);
});
options.AddPolicy(AuthorizationPolicies.AccountPolicy, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole(AuthorizationRoles.AccountRole);
});
});
builder.Services
.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<OAuthDbContext>()
.ReplaceDefaultEntities<OAuthApplication, OAuthAuthorization, OAuthScope, OAuthToken, string>();
options.AddApplicationStore<OAuthApplicationStore>();
options.ReplaceApplicationManager<OAuthApplicationManager>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
//options.DisableAccessTokenEncryption(); //uncomment this line if you wish to view the JWT payload in https://jwt.io/
options.EnableDegradedMode();
options.DisableScopeValidation();
// Enable the token endpoint.
options.SetTokenEndpointUris(CommonParameters.TokenEndPoint);
// Enable the client credentials flow.
options.AllowClientCredentialsFlow();
options.RegisterScopes("account");
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableTokenEndpointPassthrough();
// Custom Token Request Validation
options.AddEventHandler<ValidateTokenRequestContext>(builder =>
builder.UseScopedHandler<ValidateTokenRequestHandler>());
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
var scopes = context.Request.GetScopes();
Console.WriteLine("HandleTokenRequestContext");
return default;
}));
options.AddEventHandler<ValidateAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
Console.WriteLine("ValidateAuthorizationRequestContext");
return default;
}));
//Custom Handle Authorization Request
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
//context.Reject(error: "Invalid Client", description: "The specified 'client_id' doesn't match a registered application.");
Console.WriteLine("HandleAuthorizationRequestContext");
return default;
}));
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
// Register the worker responsible for seeding the database.
// Note: in a real world application, this step should be part of a setup script.
builder.Services.AddHostedService<Worker>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
ValidateTokenRequestHandler.cs
public class ValidateTokenRequestHandler : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IAccountService _accountService;
public ValidateTokenRequestHandler(IOpenIddictApplicationManager applicationManager,
IAccountService accountService)
{
_applicationManager = applicationManager;
_accountService = accountService;
}
public async ValueTask HandleAsync(ValidateTokenRequestContext context)
{
var application = await _applicationManager.FindByClientIdAsync(CommonParameters.ClientId);
if (application is not null && application is OAuthApplication oauthApplication)
{
//The ClientSecret is hashed (using PBKDF with HMAC-SHA256) before it is stored in the database.
//You can't retrieve the original secret once it's stored in the database, for obvious security reasons.
//See: https://github.com/openiddict/openiddict-core/issues/418#issuecomment-315090786
//The best option to verify that the ClientSecret is correct is to use the same Third Party CryptoHelper to verify
//that the hash and password are a cryptographic match.
var isMatch = false;
try
{
isMatch = CryptoHelper.Crypto.VerifyHashedPassword(oauthApplication.ClientSecret, context.ClientSecret);
}
catch
{
isMatch = false;
}
if (!isMatch)
{
context.Reject(error: "invalid_grant", description: "Client credentials are invalid.");
return;
}
var partnerId = oauthApplication.PartnerId;
Guid? accountId = null;
const string AccountScopeKey = "account:";
var accountScopes = context.Request.GetScopes().Where(s => s.StartsWith(AccountScopeKey)).ToList();
if (accountScopes.Count > 1)
{
// Tokens cannot be scoped to multiple accounts.
context.Reject(error: "invalid_grant", description: "Only one account scope can be provided.");
return;
}
if (accountScopes.Count == 1)
{
var account = await _accountService.FindAsync(partnerId, accountScopes[0].Substring(AccountScopeKey.Length));
if (account?.Status != AccountStatus.Active)
{
context.Reject(error: "invalid_scope", description: "Account not found.");
return;
}
accountId = account?.Id;
if (accountId.HasValue)
{
context.Request.SetParameter("AccountId", accountId.Value.ToString());
}
}
return;
}
context.Reject(error: "invalid_grant", description: "Client credentials are invalid.");
return;
}
}

how to include the role to the JWT token returning?

What I have in my mind when navigating through the application, I want to save the token to the localhost along with role name and I will check if the users have access to a certain link. Is that how it works? with Authgard in Angular 8?. Can you give me some insight of navigating an application with the role from Identity(which is built in from ASP.net core 3.1).
login
// POST api/auth/login
[HttpPost("login")]
public async Task<IActionResult> Post([FromBody]CredentialsViewModel credentials)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
if (identity == null)
{
//return null;
return BadRequest(Error.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
}
var jwt = await Tokens.GenerateJwt(identity, _jwtFactory, credentials.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented });
return new OkObjectResult(jwt);
}
Generate Token Method
public static async Task<string> GenerateJwt(ClaimsIdentity identity, IJwtFactory jwtFactory, string userName, JwtIssuerOptions jwtOptions, JsonSerializerSettings serializerSettings)
{
var response = new
{
id = identity.Claims.Single(c => c.Type == "id").Value,
//probably here I want to send the role too!!
auth_token = await jwtFactory.GenerateEncodedToken(userName, identity),
expires_in = (int)jwtOptions.ValidFor.TotalSeconds
};
return JsonConvert.SerializeObject(response, serializerSettings);
}
}
You need to add claims information when generating your JWT.
Here`s an example
And another one:
1 part(how to implement JWT), 2 part(about claims here)

IdentityServer 4 - Custom IExtensionGrantValidator always return invalid_grant

My app requirements is to authenticate using client credentials AND another code (hash).
I followed this link to create and use custom IExtensionGrantValidator.
I manged to invoke the custom IExtensionGrantValidator with approved grant, but client always gets invalid_grant error.
For some reason the set operation ofd Result (property of ExtensionGrantValidationContext) always fails (overriding the Error value returns the overrided value to client).
This is CustomGrantValidator Code:
public class CustomGrantValidator : IExtensionGrantValidator
{
public string GrantType => "grant-name";
public Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result
}
}
Startup.cs contains this line:
services.AddTransient<IExtensionGrantValidator, CustomGrantValidator>();
And finally client's code:
var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:5000") };
var disco = await httpClient.GetDiscoveryDocumentAsync("http://localhost:5000");
var cReq = await httpClient.RequestTokenAsync(new TokenRequest
{
GrantType = "grant-name",
Address = disco.TokenEndpoint,
ClientId = clientId,// client Id taken from appsetting.json
ClientSecret = clientSecret, //client secret taken from appsetting.json
Parameters = new Dictionary<string, string> { { "hash", hash } }
});
if (cReq.IsError)
//always getting 'invalid_grant' error
throw InvalidOperationException($"{cReq.Error}: {cReq.ErrorDescription}");
The below codes works on my environment :
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result;
return;
}
Don't forget to register the client to allow the custom grant :
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = { "grant-name" },
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
I got the same issue and found the answer from #Sarah Lissachell, turn out that I need to implement the IProfileService. This interface has a method called IsActiveAsync. If you don't implement this method, the answer of ValidateAsync will always be false.
public class IdentityProfileService : IProfileService
{
//This method comes second
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//IsActiveAsync turns out to be true
//Here you add the claims that you want in the access token
var claims = new List<Claim>();
claims.Add(new Claim("ThisIsNotAGoodClaim", "MyCrapClaim"));
context.IssuedClaims = claims;
}
//This method comes first
public async Task IsActiveAsync(IsActiveContext context)
{
bool isActive = false;
/*
Implement some code to determine that the user is actually active
and set isActive to true
*/
context.IsActive = isActive;
}
}
Then you have to add this implementation in your startup page.
public void ConfigureServices(IServiceCollection services)
{
// Some other code
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<Users>()
.AddInMemoryApiResources(config.GetApiResources())
.AddExtensionGrantValidator<CustomGrantValidator>()
.AddProfileService<IdentityProfileService>();
// More code
}

itfoxtex saml mvccore, attribute replace NameID

I cannot figure out how to get an attribute from the the saml response in place of the NameID value.
My IDP team is returning the value I need in an attribute rather than in NameID(which they wont budge on).
Thanks for any help!
I am running MVC Core. I have everything setup and running for NameID from the example 'TestWebAppCore' for ITfoxtec.Identity.Saml2.
I am trying to get this value in place of NameID for the session username:
saml:AttributeStatement>
<saml:Attribute Name="valueName"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
>
<saml:AttributeValue>IDValue</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success) {
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(),
saml2AuthnResponse);
try {
await saml2AuthnResponse.CreateSession(HttpContext,
claimsTransform: (claimsPrincipal) =>
ClaimsTransform.Transform(claimsPrincipal));
}
catch (Exception ex) {
log.writeLog(ex.Message.ToString());
}
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl)
? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}
It is probably not possible to logout without the NameID but you can login without.
In .NET the NameID is translated into the ClaimTypes.NameIdentifier claim. The users claims is handled in the ClaimsTransform.CreateClaimsPrincipal method.
You can either translate the incoming custom claim "valueName" to a ClaimTypes.NameIdentifier claim:
private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
var claims = new List<Claim>();
claims.AddRange(GetSaml2LogoutClaims(incomingPrincipal));
claims.Add(new Claim(ClaimTypes.NameIdentifier, GetClaimValue(incomingPrincipal, "valueName")));
return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
});
}
Or change the identity claim in the ClaimsIdentity to the incoming custom claim "valueName":
private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
var claims = new List<Claim>();
// All claims
claims.AddRange(incomingPrincipal.Claims);
return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, "valueName", ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
});
}

Basic Authentication Middleware with OWIN and ASP.NET WEB API

I created an ASP.NET WEB API 2.2 project. I used the Windows Identity Foundation based template for individual accounts available in visual studio see it here.
The web client (written in angularJS) uses OAUTH implementation with web browser cookies to store the token and the refresh token. We benefit from the helpful UserManager and RoleManager classes for managing users and their roles.
Everything works fine with OAUTH and the web browser client.
However, for some retro-compatibility concerns with desktop based clients I also need to support Basic authentication. Ideally, I would like the [Authorize], [Authorize(Role = "administrators")] etc. attributes to work with both OAUTH and Basic authentication scheme.
Thus, following the code from LeastPrivilege I created an OWIN BasicAuthenticationMiddleware that inherits from AuthenticationMiddleware.
I came to the following implementation. For the BasicAuthenticationMiddleWare only the Handler has changed compared to the Leastprivilege's code. Actually we use ClaimsIdentity rather than a series of Claim.
class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly string _challenge;
public BasicAuthenticationHandler(BasicAuthenticationOptions options)
{
_challenge = "Basic realm=" + options.Realm;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var authzValue = Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var token = authzValue.Substring("Basic ".Length).Trim();
var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
if (claimsIdentity == null)
{
return null;
}
else
{
Request.User = new ClaimsPrincipal(claimsIdentity);
return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.AppendValues("WWW-Authenticate", _challenge);
}
}
return Task.FromResult<object>(null);
}
async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
BasicAuthenticationMiddleware.CredentialValidationFunction validate)
{
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
return null;
}
var username = pair.Substring(0, ix);
var pw = pair.Substring(ix + 1);
return await validate(username, pw);
}
Then in Startup.Auth I declare the following delegate for validating authentication (simply checks if the user exists and if the password is right and generates the associated ClaimsIdentity)
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(DbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
{
using (DbContext dbContext = new DbContext())
using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
{
var user = userManager.FindByName(userName);
if (user == null)
{
return null;
}
bool ok = userManager.CheckPassword(user, password);
if (!ok)
{
return null;
}
ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
return Task.FromResult(claimsIdentity);
}
};
var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
app.UseBasicAuthentication(basicAuthOptions);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
//If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
However, even with settings the Request.User in Handler's AuthenticationAsyncCore method the [Authorize] attribute does not work as expected: responding with error 401 unauthorized every time I try to use the Basic Authentication scheme.
Any idea on what is going wrong?
I found out the culprit, in the WebApiConfig.cs file the 'individual user' template inserted the following lines.
//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Thus we also have to register our BasicAuthenticationMiddleware
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));
where BasicAuthenticationType is the constant string "Basic" that is passed to the base constructor of BasicAuthenticationOptions
public class BasicAuthenticationOptions : AuthenticationOptions
{
public const string BasicAuthenticationType = "Basic";
public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }
public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
: base(BasicAuthenticationType)
{
CredentialValidationFunction = validationFunction;
}
}