Browse Source

Implement userinfo support

pull/19/head
Kévin Chalet 10 years ago
parent
commit
61123d4dff
  1. 1
      samples/Mvc.Client/Startup.cs
  2. 24
      src/OpenIddict.Core/OpenIddictController.cs
  3. 7
      src/OpenIddict.Core/OpenIddictManager.cs
  4. 88
      src/OpenIddict.Core/OpenIddictProvider.cs
  5. 6
      src/OpenIddict.Core/Views/Shared/Authorize.cshtml

1
samples/Mvc.Client/Startup.cs

@ -52,7 +52,6 @@ namespace Mvc.Client {
options.Resource = "http://localhost:54540/"; options.Resource = "http://localhost:54540/";
options.Scope.Add("email"); options.Scope.Add("email");
options.Scope.Add("profile");
// Note: by default, IdentityModel beta8 now refuses to initiate non-HTTPS calls. // Note: by default, IdentityModel beta8 now refuses to initiate non-HTTPS calls.
// To work around this limitation, the configuration manager is manually // To work around this limitation, the configuration manager is manually

24
src/OpenIddict.Core/OpenIddictController.cs

@ -115,14 +115,28 @@ namespace OpenIddict {
var identity = new ClaimsIdentity(Options.AuthenticationScheme); var identity = new ClaimsIdentity(Options.AuthenticationScheme);
identity.AddClaim(ClaimTypes.NameIdentifier, await Manager.GetUserIdAsync(user)); identity.AddClaim(ClaimTypes.NameIdentifier, await Manager.GetUserIdAsync(user));
// Only add the name claim if the "profile" scope was present in the token request. // Resolve the username and the email address associated with the user.
if (request.GetScopes().Contains("profile", StringComparer.OrdinalIgnoreCase)) { var username = await Manager.GetUserNameAsync(user);
identity.AddClaim(ClaimTypes.Name, await Manager.GetUserNameAsync(user), destination: "id_token token"); 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. // Only add the email address if the "email" scope was present in the token request.
if (request.GetScopes().Contains("email", StringComparer.OrdinalIgnoreCase)) { if (request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) {
identity.AddClaim(ClaimTypes.Email, await Manager.GetEmailAsync(user), destination: "id_token token"); identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token");
} }
// Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application // Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application

7
src/OpenIddict.Core/OpenIddictManager.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity;
@ -43,6 +44,12 @@ namespace OpenIddict {
return Store.FindApplicationByLogoutRedirectUri(url, Context.RequestAborted); return Store.FindApplicationByLogoutRedirectUri(url, Context.RequestAborted);
} }
public virtual async Task<string> FindClaimAsync(TUser user, string type) {
return (from claim in await GetClaimsAsync(user)
where string.Equals(claim.Type, type, StringComparison.Ordinal)
select claim.Value).FirstOrDefault();
}
public virtual Task<string> GetApplicationTypeAsync(TApplication application) { public virtual Task<string> GetApplicationTypeAsync(TApplication application) {
if (application == null) { if (application == null) {
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));

88
src/OpenIddict.Core/OpenIddictProvider.cs

@ -216,13 +216,16 @@ namespace OpenIddict {
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user)); identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user));
// Only add the name claim if the "profile" scope was present in the token request. // Only add the name claim if the "profile" scope was present in the authorization request.
if (context.Request.GetScopes().Contains("profile", StringComparer.OrdinalIgnoreCase)) { // Note: filtering the username is not needed at this stage as OpenIddictController.Accept
// and OpenIddictProvider.GrantResourceOwnerCredentials are expected to reject requests that
// don't include the "email" scope if the username corresponds to the registed email address.
if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Profile)) {
identity.AddClaim(ClaimTypes.Name, await manager.GetUserNameAsync(user), destination: "id_token token"); identity.AddClaim(ClaimTypes.Name, await manager.GetUserNameAsync(user), destination: "id_token token");
} }
// Only add the email address if the "email" scope was present in the token request. // Only add the email address if the "email" scope was present in the authorization request.
if (context.Request.GetScopes().Contains("email", StringComparer.OrdinalIgnoreCase)) { if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) {
identity.AddClaim(ClaimTypes.Email, await manager.GetEmailAsync(user), destination: "id_token token"); identity.AddClaim(ClaimTypes.Email, await manager.GetEmailAsync(user), destination: "id_token token");
} }
@ -234,6 +237,60 @@ namespace OpenIddict {
context.HandleResponse(); context.HandleResponse();
} }
public override async Task ProfileEndpoint([NotNull] ProfileEndpointContext context) {
var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>();
var principal = context.AuthenticationTicket?.Principal;
Debug.Assert(principal != null);
// Note: user may be null if the user has been removed.
// In this case, return a 400 response.
var user = await manager.FindByIdAsync(principal.GetUserId());
if (user == null) {
context.Response.StatusCode = 400;
context.HandleResponse();
return;
}
// Note: "sub" is a mandatory claim.
// See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
context.Subject = await manager.GetUserIdAsync(user);
// Only add the "preferred_username" claim if the "profile" scope was present in the access token.
// Note: filtering the username is not needed at this stage as OpenIddictController.Accept
// and OpenIddictProvider.GrantResourceOwnerCredentials are expected to reject requests that
// don't include the "email" scope if the username corresponds to the registed email address.
if (principal.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIdConnectConstants.Scopes.Profile)) {
context.PreferredUsername = await manager.GetUserNameAsync(user);
context.FamilyName = await manager.FindClaimAsync(user, ClaimTypes.Surname);
context.GivenName = await manager.FindClaimAsync(user, ClaimTypes.GivenName);
context.BirthDate = await manager.FindClaimAsync(user, ClaimTypes.DateOfBirth);
}
// Only add the email address details if the "email" scope was present in the access token.
if (principal.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIdConnectConstants.Scopes.Email)) {
context.Email = await manager.GetEmailAsync(user);
// Only add the "email_verified" claim
// if the email address is non-null.
if (!string.IsNullOrEmpty(context.Email)) {
context.EmailVerified = await manager.IsEmailConfirmedAsync(user);
}
};
// Only add the phone number details if the "phone" scope was present in the access token.
if (principal.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIdConnectConstants.Scopes.Phone)) {
context.PhoneNumber = await manager.GetPhoneNumberAsync(user);
// Only add the "phone_number_verified"
// claim if the phone number is non-null.
if (!string.IsNullOrEmpty(context.PhoneNumber)) {
context.PhoneNumberVerified = await manager.IsPhoneNumberConfirmedAsync(user);
}
}
}
public override async Task GrantResourceOwnerCredentials([NotNull] GrantResourceOwnerCredentialsContext context) { public override async Task GrantResourceOwnerCredentials([NotNull] GrantResourceOwnerCredentialsContext context) {
var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>(); var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>();
@ -282,14 +339,29 @@ namespace OpenIddict {
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user)); 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 token request. // Only add the name claim if the "profile" scope was present in the token request.
if (context.Request.GetScopes().Contains("profile", StringComparer.OrdinalIgnoreCase)) { if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Profile)) {
identity.AddClaim(ClaimTypes.Name, await manager.GetUserNameAsync(user), destination: "id_token token"); // Return an error if the username corresponds to the registered
// email address and if the "email" scope has not been requested.
if (!context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email) &&
string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) {
context.Rejected(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'email' scope is required.");
return;
}
identity.AddClaim(ClaimTypes.Name, username, destination: "id_token token");
} }
// Only add the email address if the "email" scope was present in the token request. // Only add the email address if the "email" scope was present in the token request.
if (context.Request.GetScopes().Contains("email", StringComparer.OrdinalIgnoreCase)) { if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) {
identity.AddClaim(ClaimTypes.Email, await manager.GetEmailAsync(user), destination: "id_token token"); identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token");
} }
context.Validated(new ClaimsPrincipal(identity)); context.Validated(new ClaimsPrincipal(identity));

6
src/OpenIddict.Core/Views/Shared/Authorize.cshtml

@ -11,7 +11,11 @@
<form enctype="application/x-www-form-urlencoded" method="post"> <form enctype="application/x-www-form-urlencoded" method="post">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<input type="hidden" name="unique_id" value="@Model.Item1.GetUniqueIdentifier()" />
@foreach (var parameter in Model.Item1.Parameters) {
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input formaction="@Url.Action("Accept")" class="btn btn-lg btn-success" name="Authorize" type="submit" value="Yeah, sure" /> <input formaction="@Url.Action("Accept")" class="btn btn-lg btn-success" name="Authorize" type="submit" value="Yeah, sure" />
<input formaction="@Url.Action("Deny")" class="btn btn-lg btn-danger" name="Deny" type="submit" value="Hell, no" /> <input formaction="@Url.Action("Deny")" class="btn btn-lg btn-danger" name="Deny" type="submit" value="Hell, no" />
</form> </form>

Loading…
Cancel
Save