Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

450 lines
20 KiB

/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Core;
namespace OpenIddict
{
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
private async Task CreateAuthorizationAsync(
[NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictOptions options,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request)
{
if (options.DisableTokenRevocation)
{
return;
}
var descriptor = new OpenIddictAuthorizationDescriptor
{
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = OpenIddictConstants.AuthorizationTypes.AdHoc
};
foreach (var scope in request.GetScopes())
{
descriptor.Scopes.Add(scope);
}
// If the client application is known, bind it to the authorization.
if (!string.IsNullOrEmpty(request.ClientId))
{
var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted);
}
var authorization = await Authorizations.CreateAsync(descriptor, context.RequestAborted);
if (authorization != null)
{
var identifier = await Authorizations.GetIdAsync(authorization, context.RequestAborted);
if (string.IsNullOrEmpty(request.ClientId))
{
Logger.LogInformation("An ad hoc authorization was automatically created and " +
"associated with an unknown application: {Identifier}.", identifier);
}
else
{
Logger.LogInformation("An ad hoc authorization was automatically created and " +
"associated with the '{ClientId}' application: {Identifier}.",
request.ClientId, identifier);
}
// Attach the unique identifier of the ad hoc authorization to the authentication ticket
// so that it is attached to all the derived tokens, allowing batched revocations support.
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, identifier);
}
}
private async Task<string> CreateTokenAsync(
[NotNull] string type, [NotNull] AuthenticationTicket ticket,
[NotNull] OpenIddictOptions options, [NotNull] HttpContext context,
[NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens),
"Token revocation cannot be disabled when using reference tokens.");
Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken ||
type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
type == OpenIdConnectConstants.TokenUsages.RefreshToken,
"Only authorization codes, access and refresh tokens should be created using this method.");
// When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed
// and must exactly match the expiration date of the refresh token used in the token request.
if (request.IsTokenRequest() && request.IsRefreshTokenGrantType() &&
!options.UseSlidingExpiration && type == OpenIdConnectConstants.TokenUsages.RefreshToken)
{
var properties = request.GetProperty<AuthenticationTicket>(
OpenIddictConstants.Properties.AuthenticationTicket)?.Properties;
Debug.Assert(properties != null, "The authentication properties shouldn't be null.");
ticket.Properties.ExpiresUtc = properties.ExpiresUtc;
}
if (options.DisableTokenRevocation)
{
return null;
}
var descriptor = new OpenIddictTokenDescriptor
{
AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId),
CreationDate = ticket.Properties.IssuedUtc,
ExpirationDate = ticket.Properties.ExpiresUtc,
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = type
};
string result = null;
// When reference tokens are enabled or when the token is an authorization code or a
// refresh token, remove the unnecessary properties from the authentication ticket.
if (options.UseReferenceTokens ||
(type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
type == OpenIdConnectConstants.TokenUsages.RefreshToken))
{
ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null;
ticket.RemoveProperty(OpenIddictConstants.Properties.AuthorizationId)
.RemoveProperty(OpenIdConnectConstants.Properties.TokenId);
}
// If reference tokens are enabled, create a new entry for
// authorization codes, refresh tokens and access tokens.
if (options.UseReferenceTokens)
{
// Note: the data format is automatically replaced at startup time to ensure
// that encrypted tokens stored in the database cannot be considered as
// valid tokens if the developer decides to disable reference tokens support.
descriptor.Ciphertext = format.Protect(ticket);
// Generate a new crypto-secure random identifier that will be
// substituted to the ciphertext returned by the data format.
var bytes = new byte[256 / 8];
options.RandomNumberGenerator.GetBytes(bytes);
result = Base64UrlEncoder.Encode(bytes);
// Compute the digest of the generated identifier and use
// it as the hashed identifier of the reference token.
// Doing that prevents token identifiers stolen from
// the database from being used as valid reference tokens.
using (var algorithm = SHA256.Create())
{
descriptor.Hash = Convert.ToBase64String(algorithm.ComputeHash(bytes));
}
}
// Otherwise, only create a token metadata entry for authorization codes and refresh tokens.
else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode &&
type != OpenIdConnectConstants.TokenUsages.RefreshToken)
{
return null;
}
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(request.ClientId))
{
var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted);
}
// If a null value was returned by CreateAsync(), return immediately.
var token = await Tokens.CreateAsync(descriptor, context.RequestAborted);
if (token == null)
{
return null;
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty.");
}
// Restore the token identifier using the unique
// identifier attached with the database entry.
ticket.SetTokenId(identifier);
// Dynamically set the creation and expiration dates.
ticket.Properties.IssuedUtc = descriptor.CreationDate;
ticket.Properties.ExpiresUtc = descriptor.ExpirationDate;
// Restore the authorization identifier using the identifier attached with the database entry.
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, descriptor.AuthorizationId);
if (!string.IsNullOrEmpty(result))
{
Logger.LogTrace("A new reference token was successfully generated and persisted " +
"in the database: {Token} ; {Claims} ; {Properties}.",
result, ticket.Principal.Claims, ticket.Properties.Items);
}
return result;
}
private async Task<AuthenticationTicket> ReceiveTokenAsync(
[NotNull] string value, [NotNull] OpenIddictOptions options,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
if (!options.UseReferenceTokens)
{
return null;
}
string hash;
try
{
// Compute the digest of the received token and use it
// to retrieve the reference token from the database.
using (var algorithm = SHA256.Create())
{
hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value)));
}
}
// Swallow format-related exceptions to ensure badly formed
// or tampered tokens don't cause an exception at this stage.
catch
{
return null;
}
// Retrieve the token entry from the database. If it
// cannot be found, assume the token is not valid.
var token = await Tokens.FindByHashAsync(hash, context.RequestAborted);
if (token == null)
{
Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " +
"identifier cannot be found in the database.", hash);
return null;
}
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " +
"This may indicate that the token entry is corrupted.");
return null;
}
// Extract the encrypted payload from the token. If it's null or empty,
// assume the token is not a reference token and consider it as invalid.
var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(ciphertext))
{
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " +
"This may indicate that the token is not a reference token.", identifier);
return null;
}
var ticket = format.Unprotect(ciphertext);
if (ticket == null)
{
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " +
"This may indicate that the token entry is corrupted or tampered.",
await Tokens.GetIdAsync(token, context.RequestAborted));
return null;
}
// Restore the token identifier using the unique
// identifier attached with the database entry.
ticket.SetTokenId(identifier);
// Dynamically set the creation and expiration dates.
ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted);
ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted);
// Restore the authorization identifier using the identifier attached with the database entry.
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId,
await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted));
Logger.LogTrace("The reference token '{Identifier}' was successfully retrieved " +
"from the database and decrypted: {Claims} ; {Properties}.",
identifier, ticket.Principal.Claims, ticket.Properties.Items);
return ticket;
}
private async Task<bool> TryRevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context)
{
// Note: if the authorization identifier or the authorization itself
// cannot be found, return true as the authorization doesn't need
// to be revoked if it doesn't exist or is already invalid.
var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (string.IsNullOrEmpty(identifier))
{
return true;
}
var authorization = await Authorizations.FindByIdAsync(identifier, context.RequestAborted);
if (authorization == null)
{
return true;
}
try
{
await Authorizations.RevokeAsync(authorization, context.RequestAborted);
Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier);
return true;
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An exception occurred while trying to revoke the authorization " +
"associated with the token '{Identifier}'.", identifier);
return false;
}
}
private async Task<bool> TryRevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context)
{
// Note: if the authorization identifier is null, return true as no tokens need to be revoked.
var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (string.IsNullOrEmpty(identifier))
{
return true;
}
foreach (var token in await Tokens.FindByAuthorizationIdAsync(identifier, context.RequestAborted))
{
// Don't overwrite the status of the token used in the token request.
if (string.Equals(ticket.GetTokenId(), await Tokens.GetIdAsync(token, context.RequestAborted)))
{
continue;
}
try
{
await Tokens.RevokeAsync(token, context.RequestAborted);
Logger.LogInformation("The token '{Identifier}' was automatically revoked.",
await Tokens.GetIdAsync(token, context.RequestAborted));
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An exception occurred while trying to revoke the tokens " +
"associated with the token '{Identifier}'.",
await Tokens.GetIdAsync(token, context.RequestAborted));
return false;
}
}
return true;
}
private async Task<bool> TryRedeemTokenAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context)
{
// Note: if the token identifier or the token itself
// cannot be found, return true as the token doesn't need
// to be revoked if it doesn't exist or is already invalid.
var identifier = ticket.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return true;
}
var token = await Tokens.FindByIdAsync(identifier, context.RequestAborted);
if (token == null)
{
return true;
}
try
{
await Tokens.RedeemAsync(token, context.RequestAborted);
Logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier);
return true;
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An exception occurred while trying to " +
"redeem the token '{Identifier}'.", identifier);
return false;
}
}
private async Task<bool> TryExtendTokenAsync(
[NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context, [NotNull] OpenIddictOptions options)
{
var identifier = ticket.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return false;
}
var token = await Tokens.FindByIdAsync(identifier, context.RequestAborted);
if (token == null)
{
return false;
}
try
{
// Compute the new expiration date of the refresh token.
var date = options.SystemClock.UtcNow;
date += ticket.GetRefreshTokenLifetime() ?? options.RefreshTokenLifetime;
await Tokens.ExtendAsync(token, date, context.RequestAborted);
Logger.LogInformation("The expiration date of the refresh token '{Identifier}' " +
"was automatically updated: {Date}.", identifier, date);
return true;
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An exception occurred while trying to update the " +
"expiration date of the token '{Identifier}'.", identifier);
return false;
}
}
}
}