This is more of a PostMan and OpenIddict support question. You need read the PostMan docs to configure PostMan for the OAuth flow you are trying to test.
Stuck in authorization login loop
Michael Mastro II
56
Reputation points
I am trying to ensure that my authorization controller is working from Postman, but I am stuck in a loop that goes to the login page to asking if I want to authorize the application, back to the login page, back to the authorize view, etc. If I look at the output it shows successful authorization and user logged in. Here is the output from the console:
OpenIddict.Server.OpenIddictServerDispatcher: Information: The request address matched a server endpoint: Authorization.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully extracted: {
"api-version": "0.10.alpha",
"response_type": "code",
"client_id": "postman",
"state": "1234",
"scope": "openid roles DatabaseName",
"redirect_uri": "https://oauth.pstmn.io/v1/callback",
"code_challenge": "mhYtC3vtfGLqGRQOt9Q3ScYvrUKqt3HAag0OlN-UsKU",
"code_challenge_method": "S256"
}.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully validated.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The request address matched a server endpoint: Authorization.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully extracted: {
"api-version": "0.10.alpha",
"response_type": "code",
"client_id": "postman",
"state": "1234",
"scope": "openid roles DatabaseName",
"redirect_uri": "https://oauth.pstmn.io/v1/callback",
"code_challenge": "mhYtC3vtfGLqGRQOt9Q3ScYvrUKqt3HAag0OlN-UsKU",
"code_challenge_method": "S256",
"submit.Accept": "Yes"
}.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully validated.
IdentityApi.WebApi.Areas.Identity.Pages.Account.LoginModel: Information: User logged in.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The request address matched a server endpoint: Authorization.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully extracted: {
"api-version": "0.10.alpha",
"response_type": "code",
"client_id": "postman",
"state": "1234",
"scope": "openid roles DatabaseName",
"redirect_uri": "https://oauth.pstmn.io/v1/callback",
"code_challenge": "mhYtC3vtfGLqGRQOt9Q3ScYvrUKqt3HAag0OlN-UsKU",
"code_challenge_method": "S256"
}.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully validated.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The request address matched a server endpoint: Authorization.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully extracted: {
"api-version": "0.10.alpha",
"response_type": "code",
"client_id": "postman",
"state": "1234",
"scope": "openid roles DatabaseName",
"redirect_uri": "https://oauth.pstmn.io/v1/callback",
"code_challenge": "mhYtC3vtfGLqGRQOt9Q3ScYvrUKqt3HAag0OlN-UsKU",
"code_challenge_method": "S256",
"submit.Accept": "Yes"
}.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The authorization request was successfully validated.
Here is my Authorization Controller:
[Route("api/[controller]/[action]")]
[ApiController]
[ApiVersion("0.10.alpha")]
[Produces(MediaTypeNames.Application.Json)]
public class AuthorizationController : Controller
{
private readonly IConfiguration _configuration;
private readonly IdentDbContext _context;
private readonly ApplicationUserManager _userManager;
private readonly ApplicationRoleManager _roleManager;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUsers> _signInManager;
public AuthorizationController(IConfiguration configuration, IdentDbContext context, ApplicationUserManager userManager,
ApplicationRoleManager roleManager, IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager, SignInManager<ApplicationUsers> signInManager)
{
_configuration = configuration;
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
}
/// <summary>
/// Authorizes a user?
/// </summary> ///
/// <returns>a header </returns>
/// <response code="200">Returns the redirect Url and state</response>
/// <response code="400">If the redirect Url and state could not be built</response>
[HttpGet(Name = nameof(Authorize))]
[HttpPost(Name = nameof(Authorize))]
[IgnoreAntiforgeryToken]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
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(IdentityConstants.ApplicationScheme);
if (result == null || !result.Succeeded)
{
// If the client application requested promptless authentication,
// return an error indication that the user is not logged in.
if (request.HasPrompt(Prompts.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: 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(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: 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(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// Here is where I go off the rails potenitally (in the password grant I would do my BuildUserAsync(request))
// Retrieve the profile of the logged in user
var user = await _userManager.GetUserAsync(result.Principal) ??
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: await _userManager.GetUserIdAsync(user),
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 the sysadmin),
// immediately return an error if no auhtorizations can be found in the database.
case ConsentTypes.External when !authorizations.Any():
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: 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."
}));
// If the consent is implicit or if 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 scopes
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scope before calling SetScopes.
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.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();
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject: await _userManager.GetUserIdAsync(user),
client: await _applicationManager.GetIdAsync(application),
type: AuthorizationTypes.Permanent,
scopes: principal.GetScopes());
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// If 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(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}));
default:
return View(new AuthorizeViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Scope = request.Scope
});
}
}
[Authorize, FormValueRequired("submit.Accept")]
[HttpPost("/api/Authorization/Authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// 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.");
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await _authorizationManager.FindAsync(
subject: await _userManager.GetUserIdAsync(user),
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.Any() && await _applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: 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."
}));
}
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(await _scopeManager.ListResourcesAsync(principal.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();
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject: await _userManager.GetUserIdAsync(user),
client: await _applicationManager.GetIdAsync(application),
type: AuthorizationTypes.Permanent,
scopes: principal.GetScopes());
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
[Authorize, FormValueRequired("submit.Deny")]
[HttpPost("/api/Authorization/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 IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
[HttpPost("/token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsPasswordGrantType())
{
var identity = await BuildUserAsync(request);
claimsPrincipal = new ClaimsPrincipal(identity);
// Set the list of scopes granted to the client application.
claimsPrincipal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Roles
});
foreach (var claim in claimsPrincipal.Claims)
{
claim.SetDestinations(GetDestinations(claim, claimsPrincipal));
}
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
var testData = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// 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(claimsPrincipal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in claimsPrincipal.Claims)
{
claim.SetDestinations(GetDestinations(claim, claimsPrincipal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// 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:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.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;
}
}
}
the Authorize View:
@using Microsoft.Extensions.Primitives
@model IdentityApi.WebApi.Models.AuthorizeViewModel
<div class="jumbotron">
<h1>Authorization</h1>
<p class="lead text-left">Do you want to grant <strong>@Model.ApplicationName</strong> access to your data? (scopes requested: @Model.Scope)</p>
<form asp-controller="Authorization" asp-action="Authorize" method="post">
@* Flow the request parameters so they can be received by the Accept/Reject actions: *@
@foreach (var parameter in Context.Request.HasFormContentType ?
(IEnumerable<KeyValuePair<string, StringValues>>)Context.Request.Form : Context.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
<input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />
</form>
</div>
My TestData class:
public class TestData : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public TestData(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IdentDbContext>();
await context.Database.EnsureCreatedAsync(cancellationToken);
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("postman", cancellationToken) is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "postman",
ClientSecret = "postman-secret",
ConsentType = OpenIddictConstants.ConsentTypes.Explicit,
DisplayName = "Postman",
RedirectUris = { new Uri("https://oauth.pstmn.io/v1/callback") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.Scopes.Roles,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Prefixes.Scope + "DatabaseName",
OpenIddictConstants.Permissions.ResponseTypes.Code
},
Requirements =
{
OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange
}
}, cancellationToken);
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
and my Startup class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRazorPages();
services.AddDbContext<IdentDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDB"));
options.UseOpenIddict/*<Guid>*/();
});
// Add the Identity Services we are going to be using the Application Users and the Application Roles
services.AddIdentity<ApplicationUsers, ApplicationRoles>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.SignIn.RequireConfirmedAccount = true;
config.User.RequireUniqueEmail = true;
config.Lockout.MaxFailedAccessAttempts = 3;
}).AddEntityFrameworkStores<IdentDbContext>()
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddUserManager<ApplicationUserManager>()
.AddErrorDescriber<ApplicationIdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddDefaultUI();
services.AddDataLibrary();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValida
Developer technologies ASP.NET ASP.NET Core
4,815 questions