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.
 
 
 
 
 
 

248 lines
14 KiB

/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Mvc;
using Microsoft.Extensions.Internal;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace OpenIddict {
// Note: this controller is generic and doesn't need to be marked as internal to prevent MVC from discovering it.
public class OpenIddictController<TUser, TApplication> : Controller where TUser : class where TApplication : class {
public OpenIddictController(
[NotNull] OpenIddictManager<TUser, TApplication> manager,
[NotNull] OpenIddictOptions options) {
Manager = manager;
Options = options;
}
/// <summary>
/// Gets the OpenIddict manager used by the controller.
/// </summary>
protected virtual OpenIddictManager<TUser, TApplication> Manager { get; }
/// <summary>
/// Gets the OpenIddict options used by the server.
/// </summary>
protected virtual OpenIddictOptions Options { get; }
[HttpGet, HttpPost]
public virtual async Task<IActionResult> Authorize() {
// Note: when a fatal error occurs during the request processing, an OpenID Connect response
// is prematurely forged and added to the ASP.NET context by OpenIdConnectServerHandler.
// In this case, the OpenID Connect request is null and cannot be used.
// When the user agent can be safely redirected to the client application,
// OpenIdConnectServerHandler automatically handles the error and MVC is not invoked.
// You can safely remove this part and let AspNet.Security.OpenIdConnect.Server automatically
// handle the unrecoverable errors by switching ApplicationCanDisplayErrors to false.
var response = HttpContext.GetOpenIdConnectResponse();
if (response != null) {
return View("Error", response);
}
// Extract the authorization request from the cache,
// the query string or the request form.
var request = HttpContext.GetOpenIdConnectRequest();
if (request == null) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Note: authentication could be theorically enforced at the filter level via AuthorizeAttribute
// but this authorization endpoint accepts both GET and POST requests while the cookie middleware
// only uses 302 responses to redirect the user agent to the login page, making it incompatible with POST.
// To work around this limitation, the OpenID Connect request is automatically saved in the cache and will
// be restored by the OpenID Connect server middleware after the external authentication process has been completed.
if (!User.Identities.Any(identity => identity.IsAuthenticated)) {
return new ChallengeResult(new AuthenticationProperties {
RedirectUri = Url.Action(nameof(Authorize), new {
unique_id = request.GetUniqueIdentifier()
})
});
}
// Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application
// corresponds to the client_id specified in the authorization request using
// IOpenIdConnectServerProvider.ValidateClientRedirectUri (see OpenIddictProvider.cs).
var application = await Manager.FindApplicationByIdAsync(request.ClientId);
// In theory, this null check is thus not strictly necessary. That said, a race condition
// and a null reference exception could appear here if you manually removed the application
// details from the database after the initial check made by AspNet.Security.OpenIdConnect.Server.
if (application == null) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
return View("Authorize", Tuple.Create(request, await Manager.GetDisplayNameAsync(application)));
}
[Authorize, HttpPost, ValidateAntiForgeryToken]
public virtual async Task<IActionResult> Accept() {
// Extract the authorization request from the cache,
// the query string or the request form.
var request = HttpContext.GetOpenIdConnectRequest();
if (request == null) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Retrieve the user data using the unique identifier.
var user = await Manager.FindByIdAsync(User.GetUserId());
if (user == null) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity(Options.AuthenticationScheme);
identity.AddClaim(ClaimTypes.NameIdentifier, await Manager.GetUserIdAsync(user));
// Resolve the username and the email address associated with the user.
var username = await Manager.GetUserNameAsync(user);
var email = await Manager.GetEmailAsync(user);
// Only add the name claim if the "profile" scope was present in the authorization request.
if (request.ContainsScope(OpenIdConnectConstants.Scopes.Profile)) {
// Return an error if the username corresponds to the registered
// email address and if the "email" scope has not been requested.
if (!request.ContainsScope(OpenIdConnectConstants.Scopes.Email) &&
string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.InvalidRequest,
ErrorDescription = "The 'email' scope is required."
});
}
identity.AddClaim(ClaimTypes.Name, username, destination: "id_token token");
}
// Only add the email address if the "email" scope was present in the token request.
if (request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) {
identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token");
}
// Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application
// corresponds to the client_id specified in the authorization request using
// IOpenIdConnectServerProvider.ValidateClientRedirectUri (see OpenIddictProvider.cs).
var application = await Manager.FindApplicationByIdAsync(request.ClientId);
// In theory, this null check is thus not strictly necessary. That said, a race condition
// and a null reference exception could appear here if you manually removed the application
// details from the database after the initial check made by AspNet.Security.OpenIdConnect.Server.
if (application == null) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
// Create a new ClaimsIdentity containing the claims associated with the application.
// Note: setting identity.Actor is not mandatory but can be useful to access
// the whole delegation chain from the resource server (see ResourceController.cs).
identity.Actor = new ClaimsIdentity(Options.AuthenticationScheme);
identity.Actor.AddClaim(ClaimTypes.NameIdentifier, request.ClientId);
identity.Actor.AddClaim(ClaimTypes.Name, await Manager.GetDisplayNameAsync(application), destination: "id_token token");
// This call will instruct AspNet.Security.OpenIdConnect.Server to serialize
// the specified identity to build appropriate tokens (id_token and token).
// Note: you should always make sure the identities you return contain either
// a 'sub' or a 'ClaimTypes.NameIdentifier' claim. In this case, the returned
// identities always contain the name identifier returned by the external provider.
await HttpContext.Authentication.SignInAsync(Options.AuthenticationScheme, new ClaimsPrincipal(identity));
return new EmptyResult();
}
[Authorize, HttpPost, ValidateAntiForgeryToken]
public virtual IActionResult Deny() {
// Extract the authorization request from the cache,
// the query string or the request form.
var request = HttpContext.GetOpenIdConnectRequest();
if (request == null) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Notify AspNet.Security.OpenIdConnect.Server that the authorization grant has been denied.
// Note: OpenIdConnectServerHandler will automatically take care of redirecting
// the user agent to the client application using the appropriate response_mode.
HttpContext.SetOpenIdConnectResponse(new OpenIdConnectMessage {
Error = "access_denied",
ErrorDescription = "The authorization grant has been denied by the resource owner",
RedirectUri = request.RedirectUri,
State = request.State
});
return new EmptyResult();
}
[HttpGet]
public virtual async Task<ActionResult> Logout() {
// Note: when a fatal error occurs during the request processing, an OpenID Connect response
// is prematurely forged and added to the ASP.NET context by OpenIdConnectServerHandler.
// In this case, the OpenID Connect request is null and cannot be used.
// When the user agent can be safely redirected to the client application,
// OpenIdConnectServerHandler automatically handles the error and MVC is not invoked.
// You can safely remove this part and let AspNet.Security.OpenIdConnect.Server automatically
// handle the unrecoverable errors by switching ApplicationCanDisplayErrors to false.
var response = HttpContext.GetOpenIdConnectResponse();
if (response != null) {
return View("Error", response);
}
// When invoked, the logout endpoint might receive an unauthenticated request if the server cookie has expired.
// When the client application sends an id_token_hint parameter, the corresponding identity can be retrieved
// using AuthenticateAsync or using User when the authorization server is declared as AuthenticationMode.Active.
var identity = await HttpContext.Authentication.AuthenticateAsync(Options.AuthenticationScheme);
// Extract the logout request from the ASP.NET environment.
var request = HttpContext.GetOpenIdConnectRequest();
if (request == null) {
return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
return View("Logout", Tuple.Create(request, identity));
}
[HttpPost, ValidateAntiForgeryToken]
public virtual async Task Logout(CancellationToken cancellationToken) {
// Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await HttpContext.Authentication.SignOutAsync("Microsoft.AspNet.Identity.Application");
// This call will instruct AspNet.Security.OpenIdConnect.Server to serialize
// the specified identity to build appropriate tokens (id_token and token).
// Note: you should always make sure the identities you return contain either
// a 'sub' or a 'ClaimTypes.NameIdentifier' claim. In this case, the returned
// identities always contain the name identifier returned by the external provider.
await HttpContext.Authentication.SignOutAsync(Options.AuthenticationScheme);
}
}
}