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.
297 lines
13 KiB
297 lines
13 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.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.Security.Claims;
|
|
using System.Threading.Tasks;
|
|
using AspNet.Security.OpenIdConnect.Extensions;
|
|
using AspNet.Security.OpenIdConnect.Primitives;
|
|
using AspNet.Security.OpenIdConnect.Server;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Mvc.Server.Helpers;
|
|
using Mvc.Server.Models;
|
|
using Mvc.Server.ViewModels.Authorization;
|
|
using Mvc.Server.ViewModels.Shared;
|
|
using OpenIddict.Core;
|
|
using OpenIddict.Models;
|
|
|
|
namespace Mvc.Server
|
|
{
|
|
public class AuthorizationController : Controller
|
|
{
|
|
private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager;
|
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
|
|
public AuthorizationController(
|
|
OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
|
|
SignInManager<ApplicationUser> signInManager,
|
|
UserManager<ApplicationUser> userManager)
|
|
{
|
|
_applicationManager = applicationManager;
|
|
_signInManager = signInManager;
|
|
_userManager = userManager;
|
|
}
|
|
|
|
#region Authorization code, implicit and implicit flows
|
|
// Note: to support interactive flows like the code flow,
|
|
// you must provide your own authorization endpoint action:
|
|
|
|
[Authorize, HttpGet("~/connect/authorize")]
|
|
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
|
|
{
|
|
Debug.Assert(request.IsAuthorizationRequest(),
|
|
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
|
|
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
|
|
|
|
// Retrieve the application details from the database.
|
|
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
|
|
if (application == null)
|
|
{
|
|
return View("Error", new ErrorViewModel
|
|
{
|
|
Error = OpenIdConnectConstants.Errors.InvalidClient,
|
|
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
|
|
});
|
|
}
|
|
|
|
// Flow the request_id to allow OpenIddict to restore
|
|
// the original authorization request from the cache.
|
|
return View(new AuthorizeViewModel
|
|
{
|
|
ApplicationName = application.DisplayName,
|
|
RequestId = request.RequestId,
|
|
Scope = request.Scope
|
|
});
|
|
}
|
|
|
|
[Authorize, FormValueRequired("submit.Accept")]
|
|
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Accept(OpenIdConnectRequest request)
|
|
{
|
|
Debug.Assert(request.IsAuthorizationRequest(),
|
|
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
|
|
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
|
|
|
|
// Retrieve the profile of the logged in user.
|
|
var user = await _userManager.GetUserAsync(User);
|
|
if (user == null)
|
|
{
|
|
return View("Error", new ErrorViewModel
|
|
{
|
|
Error = OpenIdConnectConstants.Errors.ServerError,
|
|
ErrorDescription = "An internal error has occurred"
|
|
});
|
|
}
|
|
|
|
// Create a new authentication ticket.
|
|
var ticket = await CreateTicketAsync(request, user);
|
|
|
|
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
|
|
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
|
|
}
|
|
|
|
[Authorize, FormValueRequired("submit.Deny")]
|
|
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
|
|
public IActionResult Deny()
|
|
{
|
|
// 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.
|
|
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
|
|
}
|
|
|
|
// Note: the logout action is only useful when implementing interactive
|
|
// flows like the authorization code flow or the implicit flow.
|
|
|
|
[HttpGet("~/connect/logout")]
|
|
public IActionResult Logout(OpenIdConnectRequest request)
|
|
{
|
|
Debug.Assert(request.IsLogoutRequest(),
|
|
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
|
|
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
|
|
|
|
// Flow the request_id to allow OpenIddict to restore
|
|
// the original logout request from the distributed cache.
|
|
return View(new LogoutViewModel
|
|
{
|
|
RequestId = request.RequestId
|
|
});
|
|
}
|
|
|
|
[HttpPost("~/connect/logout"), ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Logout()
|
|
{
|
|
// Ask ASP.NET Core Identity to delete the local and external cookies created
|
|
// when the user agent is redirected from the external identity provider
|
|
// after a successful authentication flow (e.g Google or Facebook).
|
|
await _signInManager.SignOutAsync();
|
|
|
|
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
|
|
// to the post_logout_redirect_uri specified by the client application.
|
|
return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme);
|
|
}
|
|
#endregion
|
|
|
|
#region Password, authorization code and refresh token flows
|
|
// Note: to support non-interactive flows like password,
|
|
// you must provide your own token endpoint action:
|
|
|
|
[HttpPost("~/connect/token"), Produces("application/json")]
|
|
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
|
|
{
|
|
Debug.Assert(request.IsTokenRequest(),
|
|
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
|
|
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
|
|
|
|
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."
|
|
});
|
|
}
|
|
|
|
// Create a new authentication ticket.
|
|
var ticket = await CreateTicketAsync(request, user);
|
|
|
|
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
|
|
}
|
|
|
|
else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
|
|
{
|
|
// Retrieve the claims principal stored in the authorization code/refresh token.
|
|
var info = await HttpContext.AuthenticateAsync(OpenIdConnectServerDefaults.AuthenticationScheme);
|
|
|
|
// Retrieve the user profile corresponding to the authorization code/refresh token.
|
|
// Note: if you want to automatically invalidate the authorization code/refresh token
|
|
// when the user password/roles change, use the following line instead:
|
|
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
|
|
var user = await _userManager.GetUserAsync(info.Principal);
|
|
if (user == null)
|
|
{
|
|
return BadRequest(new OpenIdConnectResponse
|
|
{
|
|
Error = OpenIdConnectConstants.Errors.InvalidGrant,
|
|
ErrorDescription = "The token is no longer valid."
|
|
});
|
|
}
|
|
|
|
// Ensure the user is still allowed to sign in.
|
|
if (!await _signInManager.CanSignInAsync(user))
|
|
{
|
|
return BadRequest(new OpenIdConnectResponse
|
|
{
|
|
Error = OpenIdConnectConstants.Errors.InvalidGrant,
|
|
ErrorDescription = "The user is no longer allowed to sign in."
|
|
});
|
|
}
|
|
|
|
// Create a new authentication ticket, but reuse the properties stored in the
|
|
// authorization code/refresh token, including the scopes originally granted.
|
|
var ticket = await CreateTicketAsync(request, user, info.Properties);
|
|
|
|
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
|
|
}
|
|
|
|
return BadRequest(new OpenIdConnectResponse
|
|
{
|
|
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
|
|
ErrorDescription = "The specified grant type is not supported."
|
|
});
|
|
}
|
|
#endregion
|
|
|
|
private async Task<AuthenticationTicket> CreateTicketAsync(
|
|
OpenIdConnectRequest request, ApplicationUser user,
|
|
AuthenticationProperties properties = null)
|
|
{
|
|
// Create a new ClaimsPrincipal containing the claims that
|
|
// will be used to create an id_token, a token or a code.
|
|
var principal = await _signInManager.CreateUserPrincipalAsync(user);
|
|
|
|
// Create a new authentication ticket holding the user identity.
|
|
var ticket = new AuthenticationTicket(principal, properties,
|
|
OpenIdConnectServerDefaults.AuthenticationScheme);
|
|
|
|
if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType())
|
|
{
|
|
// 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.
|
|
ticket.SetScopes(request.GetScopes());
|
|
ticket.SetResources("resource_server");
|
|
}
|
|
|
|
foreach (var claim in ticket.Principal.Claims)
|
|
{
|
|
claim.SetDestinations(GetDestinations(claim, ticket));
|
|
}
|
|
|
|
return ticket;
|
|
}
|
|
|
|
private IEnumerable<string> GetDestinations(Claim claim, AuthenticationTicket ticket)
|
|
{
|
|
// 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 OpenIdConnectConstants.Claims.Name:
|
|
yield return OpenIdConnectConstants.Destinations.AccessToken;
|
|
|
|
if (ticket.HasScope(OpenIdConnectConstants.Scopes.Profile))
|
|
yield return OpenIdConnectConstants.Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
case OpenIdConnectConstants.Claims.Email:
|
|
yield return OpenIdConnectConstants.Destinations.AccessToken;
|
|
|
|
if (ticket.HasScope(OpenIdConnectConstants.Scopes.Email))
|
|
yield return OpenIdConnectConstants.Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
case OpenIdConnectConstants.Claims.Role:
|
|
yield return OpenIdConnectConstants.Destinations.AccessToken;
|
|
|
|
if (ticket.HasScope(OpenIddictConstants.Claims.Roles))
|
|
yield return OpenIdConnectConstants.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 OpenIdConnectConstants.Destinations.AccessToken;
|
|
yield break;
|
|
}
|
|
}
|
|
}
|
|
}
|