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.
 
 
 
 
 
 

651 lines
29 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.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
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.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
namespace OpenIddict.Server
{
public partial class OpenIddictServerProvider : OpenIdConnectServerProvider
{
private async Task CreateAuthorizationAsync([NotNull] AuthenticationTicket ticket,
[NotNull] OpenIddictServerOptions options, [NotNull] OpenIdConnectRequest request)
{
var descriptor = new OpenIddictAuthorizationDescriptor
{
Principal = ticket.Principal,
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = OpenIddictConstants.AuthorizationTypes.AdHoc
};
foreach (var property in ticket.Properties.Items)
{
descriptor.Properties.Add(property);
}
foreach (var scope in ticket.GetScopes())
{
descriptor.Scopes.Add(scope);
}
// If the client application is known, bind it to the authorization.
if (!string.IsNullOrEmpty(request.ClientId))
{
var application = request.GetProperty($"{OpenIddictConstants.Properties.Application}:{request.ClientId}");
Debug.Assert(application != null, "The client application shouldn't be null.");
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
}
var authorization = await _authorizationManager.CreateAsync(descriptor);
if (authorization != null)
{
var identifier = await _authorizationManager.GetIdAsync(authorization);
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] OpenIddictServerOptions options,
[NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens),
"Token storage 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.DisableTokenStorage)
{
return null;
}
var descriptor = new OpenIddictTokenDescriptor
{
AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId),
CreationDate = ticket.Properties.IssuedUtc,
ExpirationDate = ticket.Properties.ExpiresUtc,
Principal = ticket.Principal,
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = type
};
foreach (var property in ticket.Properties.Items)
{
descriptor.Properties.Add(property);
}
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.Payload = 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);
// Obfuscate the reference identifier so it can be safely stored in the databse.
descriptor.ReferenceId = await _tokenManager.ObfuscateReferenceIdAsync(result);
}
// 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 = request.GetProperty($"{OpenIddictConstants.Properties.Application}:{request.ClientId}");
Debug.Assert(application != null, "The client application shouldn't be null.");
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
}
// If a null value was returned by CreateAsync(), return immediately.
// Note: the request cancellation token is deliberately not used here to ensure the caller
// cannot prevent this operation from being executed by resetting the TCP connection.
var token = await _tokenManager.CreateAsync(descriptor);
if (token == null)
{
return null;
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await _tokenManager.GetIdAsync(token);
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 type, [NotNull] string value,
[NotNull] OpenIddictServerOptions options,
[NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
Debug.Assert(!(options.DisableTokenStorage && 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 validated using this method.");
string identifier;
AuthenticationTicket ticket;
object token;
if (options.UseReferenceTokens)
{
// For introspection or revocation requests, this method may be called more than once.
// For reference tokens, this may result in multiple database calls being made.
// To optimize that, the token is added to the request properties to indicate that
// a database lookup was already made with the same identifier. If the marker exists,
// the property value (that may be null) is used instead of making a database call.
if (request.HasProperty($"{OpenIddictConstants.Properties.ReferenceToken}:{value}"))
{
token = request.GetProperty($"{OpenIddictConstants.Properties.ReferenceToken}:{value}");
}
else
{
// Retrieve the token entry from the database. If it
// cannot be found, assume the token is not valid.
token = await _tokenManager.FindByReferenceIdAsync(value);
// Store the token as a request property so it can be retrieved if this method is called another time.
request.AddProperty($"{OpenIddictConstants.Properties.ReferenceToken}:{value}", token);
}
if (token == null)
{
_logger.LogInformation("The reference token corresponding to the '{Identifier}' " +
"reference identifier cannot be found in the database.", value);
return null;
}
identifier = await _tokenManager.GetIdAsync(token);
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 payload = await _tokenManager.GetPayloadAsync(token);
if (string.IsNullOrEmpty(payload))
{
_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;
}
ticket = format.Unprotect(payload);
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 _tokenManager.GetIdAsync(token));
return null;
}
request.SetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}", token);
}
else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
type == OpenIdConnectConstants.TokenUsages.RefreshToken)
{
ticket = format.Unprotect(value);
if (ticket == null)
{
_logger.LogTrace("The received token was invalid or malformed: {Token}.", value);
return null;
}
identifier = ticket.GetTokenId();
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;
}
// For introspection or revocation requests, this method may be called more than once.
// For codes/refresh tokens, this may result in multiple database calls being made.
// To optimize that, the token is added to the request properties to indicate that
// a database lookup was already made with the same identifier. If the marker exists,
// the property value (that may be null) is used instead of making a database call.
if (request.HasProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"))
{
token = request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}");
}
// Otherwise, retrieve the authorization code/refresh token entry from the database.
// If it cannot be found, assume the authorization code/refresh token is not valid.
else
{
token = await _tokenManager.FindByIdAsync(identifier);
// Store the token as a request property so it can be retrieved if this method is called another time.
request.AddProperty($"{OpenIddictConstants.Properties.Token}:{identifier}", token);
}
if (token == null)
{
_logger.LogInformation("The token '{Identifier}' cannot be found in the database.", ticket.GetTokenId());
return null;
}
}
else
{
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 _tokenManager.GetCreationDateAsync(token);
ticket.Properties.ExpiresUtc = await _tokenManager.GetExpirationDateAsync(token);
// Restore the authorization identifier using the identifier attached with the database entry.
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId,
await _tokenManager.GetAuthorizationIdAsync(token));
_logger.LogTrace("The token '{Identifier}' was successfully decrypted and " +
"retrieved from the database: {Claims} ; {Properties}.",
ticket.GetTokenId(), ticket.Principal.Claims, ticket.Properties.Items);
return ticket;
}
private async Task<bool> TryRevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket)
{
// 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 _authorizationManager.FindByIdAsync(identifier);
if (authorization == null)
{
return true;
}
try
{
// Note: the request cancellation token is deliberately not used here to ensure the caller
// cannot prevent this operation from being executed by resetting the TCP connection.
await _authorizationManager.RevokeAsync(authorization);
_logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier);
return true;
}
catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError)
{
_logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the authorization " +
"associated with the token '{Identifier}'.", identifier);
return false;
}
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> TryRevokeTokenAsync([NotNull] object token)
{
var identifier = await _tokenManager.GetIdAsync(token);
Debug.Assert(!string.IsNullOrEmpty(identifier), "The token identifier shouldn't be null or empty.");
try
{
// Note: the request cancellation token is deliberately not used here to ensure the caller
// cannot prevent this operation from being executed by resetting the TCP connection.
await _tokenManager.RevokeAsync(token);
_logger.LogInformation("The token '{Identifier}' was automatically revoked.", identifier);
return true;
}
catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError)
{
_logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the token '{Identifier}'.", identifier);
return false;
}
catch (Exception exception)
{
_logger.LogWarning(exception, "An exception occurred while trying to revoke the token '{Identifier}'.", identifier);
return false;
}
}
private async Task<bool> TryRevokeTokensAsync([NotNull] AuthenticationTicket ticket)
{
// 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;
}
var result = true;
foreach (var token in await _tokenManager.FindByAuthorizationIdAsync(identifier))
{
// Don't change the status of the token used in the token request.
if (string.Equals(ticket.GetTokenId(), await _tokenManager.GetIdAsync(token), StringComparison.Ordinal))
{
continue;
}
result &= await TryRevokeTokenAsync(token);
}
return result;
}
private async Task<bool> TryRedeemTokenAsync([NotNull] object token)
{
var identifier = await _tokenManager.GetIdAsync(token);
Debug.Assert(!string.IsNullOrEmpty(identifier), "The token identifier shouldn't be null or empty.");
try
{
// Note: the request cancellation token is deliberately not used here to ensure the caller
// cannot prevent this operation from being executed by resetting the TCP connection.
await _tokenManager.RedeemAsync(token);
_logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier);
return true;
}
catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError)
{
_logger.LogDebug(exception, "A concurrency exception occurred while trying to redeem with the token '{Identifier}'.", identifier);
return false;
}
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] object token, [NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictServerOptions options)
{
var identifier = ticket.GetTokenId();
Debug.Assert(!string.IsNullOrEmpty(identifier), "The token identifier shouldn't be null or empty.");
try
{
// Compute the new expiration date of the refresh token.
var date = options.SystemClock.UtcNow;
date += ticket.GetRefreshTokenLifetime() ?? options.RefreshTokenLifetime;
// Note: the request cancellation token is deliberately not used here to ensure the caller
// cannot prevent this operation from being executed by resetting the TCP connection.
await _tokenManager.ExtendAsync(token, date);
_logger.LogInformation("The expiration date of the refresh token '{Identifier}' " +
"was automatically updated: {Date}.", identifier, date);
return true;
}
catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError)
{
_logger.LogDebug(exception, "A concurrency exception occurred while trying to update the " +
"expiration date of the token '{Identifier}'.", identifier);
return false;
}
catch (Exception exception)
{
_logger.LogWarning(exception, "An exception occurred while trying to update the " +
"expiration date of the token '{Identifier}'.", identifier);
return false;
}
}
private IEnumerable<(string property, string parameter, OpenIdConnectParameter value)> GetParameters(
OpenIdConnectRequest request, AuthenticationProperties properties)
{
Debug.Assert(properties != null, "The authentication properties shouldn't be null.");
Debug.Assert(request != null, "The request shouldn't be null.");
Debug.Assert(request.IsAuthorizationRequest() || request.IsLogoutRequest() || request.IsTokenRequest(),
"The request should be an authorization, logout or token request.");
foreach (var property in properties.Items)
{
if (string.IsNullOrEmpty(property.Key))
{
continue;
}
if (string.IsNullOrEmpty(property.Value))
{
continue;
}
if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Boolean))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Boolean));
bool value;
try
{
value = bool.Parse(property.Value);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "An error occurred while parsing the public property " +
"'{Name}' from the authentication ticket.", name);
continue;
}
yield return (property.Key, name, value);
}
else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Integer))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Integer));
long value;
try
{
value = long.Parse(property.Value, CultureInfo.InvariantCulture);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "An error occurred while parsing the public property " +
"'{Name}' from the authentication ticket.", name);
continue;
}
yield return (property.Key, name, value);
}
else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Json))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Json));
if (request.IsAuthorizationRequest() || request.IsLogoutRequest())
{
_logger.LogWarning("The JSON property '{Name}' was excluded as it was not " +
"compatible with the OpenID Connect response type.", name);
continue;
}
JToken value;
try
{
value = JToken.Parse(property.Value);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "An error occurred while deserializing the public JSON " +
"property '{Name}' from the authentication ticket.", name);
continue;
}
yield return (property.Key, name, value);
}
else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.String))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.String));
yield return (property.Key, name, property.Value);
}
continue;
}
}
}
}