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.
 
 
 
 
 
 

471 lines
24 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.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using OpenIddict.Abstractions;
using OpenIddict.Client;
using OpenIddict.Client.Owin;
using OpenIddict.Sandbox.AspNet.Server.Helpers;
using OpenIddict.Sandbox.AspNet.Server.ViewModels.Authorization;
using OpenIddict.Server.Owin;
using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenIddict.Sandbox.AspNet.Server.Controllers;
public class AuthorizationController : Controller
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly OpenIddictClientService _clientService;
private readonly IOpenIddictScopeManager _scopeManager;
public AuthorizationController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
OpenIddictClientService clientService,
IOpenIddictScopeManager scopeManager)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_clientService = clientService;
_scopeManager = scopeManager;
}
[HttpGet, Route("~/connect/authorize")]
public async Task<ActionResult> Authorize()
{
// Note: the request object contains all the parameters specified in the query string or request form
// or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow.
// As such, the data contained in this object MUST NOT be serialized or returned unprotected to the
// user agent (e.g as HTML hidden input fields). If only the query string or request form parameters
// need to be resolved, the Request.QueryString and Request.Form collections must be used instead.
var context = HttpContext.GetOwinContext();
var request = context.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Try to retrieve the user principal stored in the authentication cookie and redirect
// the user agent to the login page (or to an external provider) in the following cases:
//
// - If the user principal can't be extracted or the cookie is too old.
// - If prompt=login was specified by the client application.
// - If max_age=0 was specified by the client application (max_age=0 is equivalent to prompt=login).
// - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
var result = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);
if (result is not { Identity: ClaimsIdentity } ||
((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
TempData["IgnoreAuthenticationChallenge"] is null or false))
{
// To avoid endless login endpoint -> authorization endpoint redirects, a special temp data entry is
// used to skip the challenge if the user agent has already been redirected to the login endpoint.
//
// Note: this flag doesn't guarantee that the user has accepted to re-authenticate. If such a guarantee
// is needed, the existing authentication cookie MUST be deleted AND revoked (e.g using ASP.NET
// Identity's security stamp feature with an extremely short revalidation time span) before triggering
// a challenge to redirect the user agent to the login endpoint.
TempData["IgnoreAuthenticationChallenge"] = true;
// For applications that want to allow the client to select the external authentication provider
// that will be used to authenticate the user, the identity_provider parameter can be used for that.
if (!string.IsNullOrEmpty(request.IdentityProvider))
{
var registrations = await _clientService.GetClientRegistrationsAsync();
if (!registrations.Any(registration => string.Equals(registration.ProviderName,
request.IdentityProvider, StringComparison.Ordinal)))
{
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = Errors.InvalidRequest,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] =
"The specified identity provider is not valid."
}));
return new EmptyResult();
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// specifying the issuer URI or the provider name is not required.
[OpenIddictClientOwinConstants.Properties.ProviderName] = request.IdentityProvider
})
{
// Once the callback is handled, redirect the user agent to the ASP.NET Identity
// page responsible for showing the external login confirmation form if necessary.
RedirectUri = Url.Action("ExternalLoginCallback", "Account", new
{
ReturnUrl = Request.RawUrl
})
};
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType);
return new EmptyResult();
}
context.Authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie);
return new EmptyResult();
}
// Retrieve the profile of the logged in user.
var user = await context.GetUserManager<ApplicationUserManager>().FindByIdAsync(result.Identity.GetUserId()) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await _authorizationManager.FindAsync(
subject: user.Id,
client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid,
type : AuthorizationTypes.Permanent,
scopes : request.GetScopes()).ToListAsync();
switch (await _applicationManager.GetConsentTypeAsync(application))
{
// If the consent is external (e.g when authorizations are granted by a sysadmin),
// immediately return an error if no authorization can be found in the database.
case ConsentTypes.External when authorizations.Count is 0:
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}));
return new EmptyResult();
// If the consent is implicit or if an authorization was found,
// return an authorization response without displaying the consent form.
case ConsentTypes.Implicit:
case ConsentTypes.External when authorizations.Count is not 0:
case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPromptValue(PromptValues.Consent):
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: OpenIddictServerOwinDefaults.AuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens.
identity.SetClaim(Claims.Subject, user.Id)
.SetClaim(Claims.Email, user.Email)
.SetClaim(Claims.Name, user.UserName)
.SetClaim(Claims.PreferredUsername, user.UserName)
.SetClaims(Claims.Role, [.. await context.Get<ApplicationUserManager>().GetRolesAsync(user.Id)]);
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
authorization ??= await _authorizationManager.CreateAsync(
identity: identity,
subject : user.Id,
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : identity.GetScopes());
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
context.Authentication.SignIn(new AuthenticationProperties(), identity);
return new EmptyResult();
// At this point, no authorization was found in the database and an error must be returned
// if the client application specified prompt=none in the authorization request.
case ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None):
case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None):
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}));
return new EmptyResult();
// In every other case, render the consent form.
default: return View(new AuthorizeViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Scope = request.Scope,
// Flow the request parameters so they can be received by the Accept/Reject actions.
Parameters = string.Equals(Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase) ?
from name in Request.Form.AllKeys
from value in Request.Form.GetValues(name)
select new KeyValuePair<string, string>(name, value) :
from name in Request.QueryString.AllKeys
from value in Request.QueryString.GetValues(name)
select new KeyValuePair<string, string>(name, value)
});
}
}
[Authorize, FormValueRequired("submit.Accept")]
[HttpPost, Route("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<ActionResult> Accept()
{
// Note: the request object contains all the parameters specified in the query string or request form
// (or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow).
// As such, the data contained in this object MUST NOT be serialized or returned unprotected to the
// user agent (e.g as HTML hidden input fields). If only the query string or request form parameters
// need to be resolved, the Request.QueryString and Request.Form collections must be used instead.
var context = HttpContext.GetOwinContext();
var request = context.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
var result = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);
if (result == null || result.Identity == null)
{
context.Authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie);
return new EmptyResult();
}
// Retrieve the profile of the logged in user.
var user = await context.GetUserManager<ApplicationUserManager>().FindByIdAsync(result.Identity.GetUserId()) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await _authorizationManager.FindAsync(
subject: user.Id,
client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid,
type : AuthorizationTypes.Permanent,
scopes : request.GetScopes()).ToListAsync();
// Note: the same check is already made in the other action but is repeated
// here to ensure a malicious user can't abuse this POST-only endpoint and
// force it to return a valid response without the external authorization.
if (authorizations.Count is 0 && await _applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
{
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}));
return new EmptyResult();
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: OpenIddictServerOwinDefaults.AuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens.
identity.SetClaim(Claims.Subject, user.Id)
.SetClaim(Claims.Email, user.Email)
.SetClaim(Claims.Name, user.UserName)
.SetClaim(Claims.PreferredUsername, user.UserName)
.SetClaims(Claims.Role, [.. await context.Get<ApplicationUserManager>().GetRolesAsync(user.Id)]);
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
authorization ??= await _authorizationManager.CreateAsync(
identity: identity,
subject : user.Id,
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : identity.GetScopes());
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
context.Authentication.SignIn(new AuthenticationProperties(), identity);
return new EmptyResult();
}
[Authorize, FormValueRequired("submit.Deny")]
[HttpPost, Route("~/connect/authorize"), ValidateAntiForgeryToken]
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
public ActionResult Deny()
{
var context = HttpContext.GetOwinContext();
context.Authentication.Challenge(OpenIddictServerOwinDefaults.AuthenticationType);
return new EmptyResult();
}
[HttpGet, Route("~/connect/endsession")]
public ActionResult EndSession() => View(new AuthorizeViewModel
{
// Flow the request parameters so they can be received by the Accept/Reject actions.
Parameters = string.Equals(Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase) ?
from name in Request.Form.AllKeys
from value in Request.Form.GetValues(name)
select new KeyValuePair<string, string>(name, value) :
from name in Request.QueryString.AllKeys
from value in Request.QueryString.GetValues(name)
select new KeyValuePair<string, string>(name, value)
});
[ActionName(nameof(EndSession)), HttpPost, Route("~/connect/endsession"), ValidateAntiForgeryToken]
public ActionResult EndSessionPost()
{
var context = HttpContext.GetOwinContext();
context.Authentication.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
context.Authentication.SignOut(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties
{
RedirectUri = "/"
});
return new EmptyResult();
}
[HttpPost, Route("~/connect/token")]
public async Task<ActionResult> Exchange()
{
var context = HttpContext.GetOwinContext();
var request = context.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims identity stored in the authorization code/device code/refresh token.
var result = await context.Authentication.AuthenticateAsync(OpenIddictServerOwinDefaults.AuthenticationType);
// Retrieve the user profile corresponding to the authorization code/refresh token.
var user = await context.GetUserManager<ApplicationUserManager>().FindByIdAsync(result.Identity.GetClaim(Claims.Subject));
if (user == null)
{
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
return new EmptyResult();
}
// Ensure the user is still allowed to sign in.
if (context.GetUserManager<ApplicationUserManager>().IsLockedOut(user.Id))
{
context.Authentication.Challenge(
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
return new EmptyResult();
}
var identity = new ClaimsIdentity(result.Identity.Claims,
authenticationType: OpenIddictServerOwinDefaults.AuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Override the user claims present in the principal in case they
// changed since the authorization code/refresh token was issued.
identity.SetClaim(Claims.Subject, user.Id)
.SetClaim(Claims.Email, user.Email)
.SetClaim(Claims.Name, user.UserName)
.SetClaim(Claims.PreferredUsername, user.UserName)
.SetClaims(Claims.Role, [.. await context.Get<ApplicationUserManager>().GetRolesAsync(user.Id)]);
identity.SetDestinations(GetDestinations);
// Ask OpenIddict to issue the appropriate access/identity tokens.
context.Authentication.SignIn(new AuthenticationProperties(), identity);
return new EmptyResult();
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private static IEnumerable<string> GetDestinations(Claim claim)
{
// 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.
switch (claim.Type)
{
case Claims.Name or Claims.PreferredUsername:
yield return Destinations.AccessToken;
if (claim.Subject.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (claim.Subject.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (claim.Subject.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}