Browse Source

Update the server sample to use the authorization manager

pull/895/head
Kévin Chalet 6 years ago
parent
commit
2aa4c45794
  1. 196
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  2. 4
      samples/Mvc.Server/Startup.cs
  3. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  4. 19
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  5. 36
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs

196
samples/Mvc.Server/Controllers/AuthorizationController.cs

@ -6,6 +6,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
@ -14,6 +16,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Mvc.Server.Helpers;
using Mvc.Server.Models;
using Mvc.Server.ViewModels.Authorization;
@ -28,15 +31,18 @@ namespace Mvc.Server
public class AuthorizationController : Controller
{
private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager;
private readonly OpenIddictAuthorizationManager<OpenIddictAuthorization> _authorizationManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
OpenIddictAuthorizationManager<OpenIddictAuthorization> authorizationManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_signInManager = signInManager;
_userManager = userManager;
}
@ -45,21 +51,160 @@ namespace Mvc.Server
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
[Authorize, HttpGet("~/connect/authorize")]
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
// If it can't be extracted, redirect the user to the login page.
var result = await HttpContext.AuthenticateAsync();
if (result == null || !result.Succeeded)
{
// If the client application requested promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPrompt(Prompts.None))
{
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
return Challenge(new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// If prompt=login was specified by the client application,
// immediately return the user agent to the login page.
if (request.HasPrompt(Prompts.Login))
{
// To avoid endless login -> authorization redirects, the prompt=login flag
// is removed from the authorization request payload before redirecting the user.
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
var parameters = Request.HasFormContentType ?
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
return Challenge(new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
// If a max_age parameter was provided, ensure that the cookie is not too old.
// If it's too old, automatically redirect the user agent to the login page.
if (request.MaxAge != null && result.Properties.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))
{
if (request.HasPrompt(Prompts.None))
{
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
return Challenge(new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User) ??
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.");
return View(new AuthorizeViewModel
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await _authorizationManager.FindAsync(
subject: User.FindFirst(Claims.Subject)?.Value,
client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid,
type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(request.GetScopes())).ToListAsync();
switch (await _applicationManager.GetConsentTypeAsync(application))
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Scope = request.Scope
});
// 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.Any():
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// 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.Any():
case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent):
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// 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.
principal.SetScopes(request.GetScopes());
principal.SetResources("resource_server");
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject : principal.FindFirst(Claims.Subject)?.Value,
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(principal.GetScopes()));
}
principal.SetInternalAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// 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.HasPrompt(Prompts.None):
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required.",
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// In every other case, render the consent form.
default:
return View(new AuthorizeViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Scope = request.Scope
});
}
}
[Authorize, FormValueRequired("submit.Accept")]
@ -73,6 +218,32 @@ namespace Mvc.Server
var user = await _userManager.GetUserAsync(User) ??
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.FindFirst(Claims.Subject)?.Value,
client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid,
type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(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.
switch (await _applicationManager.GetConsentTypeAsync(application))
{
case ConsentTypes.External when !authorizations.Any():
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Note: in this sample, the granted scopes match the requested scope
@ -81,6 +252,21 @@ namespace Mvc.Server
principal.SetScopes(request.GetScopes());
principal.SetResources("resource_server");
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject : principal.FindFirst(Claims.Subject)?.Value,
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(principal.GetScopes()));
}
principal.SetInternalAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));

4
samples/Mvc.Server/Startup.cs

@ -182,6 +182,7 @@ namespace Mvc.Server
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = OpenIddictConstants.ConsentTypes.Explicit,
DisplayName = "MVC client application",
PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUris = { new Uri("http://localhost:53507/signin-oidc") },
@ -219,8 +220,9 @@ namespace Mvc.Server
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "postman",
ConsentType = OpenIddictConstants.ConsentTypes.Systematic,
DisplayName = "Postman",
RedirectUris = { new Uri("https://www.getpostman.com/oauth2/callback") },
RedirectUris = { new Uri("urn:postman") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -132,6 +132,7 @@ namespace OpenIddict.Abstractions
public const string Explicit = "explicit";
public const string External = "external";
public const string Implicit = "implicit";
public const string Systematic = "systematic";
}
public static class Destinations

19
src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs

@ -44,6 +44,25 @@ namespace OpenIddict.Abstractions
return GetValues(request.AcrValues, Separators.Space).Distinct(StringComparer.Ordinal).ToImmutableArray();
}
/// <summary>
/// Extracts the prompt values from an <see cref="OpenIddictRequest"/>.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
public static ImmutableArray<string> GetPrompts([NotNull] this OpenIddictRequest request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (string.IsNullOrEmpty(request.Prompt))
{
return ImmutableArray.Create<string>();
}
return GetValues(request.Prompt, Separators.Space).Distinct(StringComparer.Ordinal).ToImmutableArray();
}
/// <summary>
/// Extracts the response types from an <see cref="OpenIddictRequest"/>.
/// </summary>

36
test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs

@ -52,6 +52,42 @@ namespace OpenIddict.Abstractions.Tests.Primitives
Assert.Equal(values, request.GetAcrValues());
}
[Fact]
public void GetPrompts_ThrowsAnExceptionForNullRequest()
{
// Arrange
var request = (OpenIddictRequest) null;
// Act
var exception = Assert.Throws<ArgumentNullException>(() => request.GetPrompts());
// Assert
Assert.Equal("request", exception.ParamName);
}
[Theory]
[InlineData(null, new string[0])]
[InlineData("login", new[] { "login" })]
[InlineData("login ", new[] { "login" })]
[InlineData(" login ", new[] { "login" })]
[InlineData("login consent", new[] { "login", "consent" })]
[InlineData("login consent", new[] { "login", "consent" })]
[InlineData("login consent ", new[] { "login", "consent" })]
[InlineData(" login consent", new[] { "login", "consent" })]
[InlineData("login login consent", new[] { "login", "consent" })]
[InlineData("login LOGIN consent", new[] { "login", "LOGIN", "consent" })]
public void GetPrompts_ReturnsExpectedPrompts(string value, string[] values)
{
// Arrange
var request = new OpenIddictRequest
{
Prompt = value
};
// Act and assert
Assert.Equal(values, request.GetPrompts());
}
[Fact]
public void GetResponseTypes_ThrowsAnExceptionForNullRequest()
{

Loading…
Cancel
Save