diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index d5fa627d..07e120c4 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -52,7 +52,6 @@ namespace Mvc.Client { options.Resource = "http://localhost:54540/"; options.Scope.Add("email"); - options.Scope.Add("profile"); // Note: by default, IdentityModel beta8 now refuses to initiate non-HTTPS calls. // To work around this limitation, the configuration manager is manually diff --git a/src/OpenIddict.Core/OpenIddictController.cs b/src/OpenIddict.Core/OpenIddictController.cs index a3271bbc..a5131a30 100644 --- a/src/OpenIddict.Core/OpenIddictController.cs +++ b/src/OpenIddict.Core/OpenIddictController.cs @@ -115,14 +115,28 @@ namespace OpenIddict { var identity = new ClaimsIdentity(Options.AuthenticationScheme); identity.AddClaim(ClaimTypes.NameIdentifier, await Manager.GetUserIdAsync(user)); - // Only add the name claim if the "profile" scope was present in the token request. - if (request.GetScopes().Contains("profile", StringComparer.OrdinalIgnoreCase)) { - identity.AddClaim(ClaimTypes.Name, await Manager.GetUserNameAsync(user), destination: "id_token token"); + // 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.GetScopes().Contains("email", StringComparer.OrdinalIgnoreCase)) { - identity.AddClaim(ClaimTypes.Email, await Manager.GetEmailAsync(user), destination: "id_token token"); + if (request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) { + identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token"); } // Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application diff --git a/src/OpenIddict.Core/OpenIddictManager.cs b/src/OpenIddict.Core/OpenIddictManager.cs index 5017985e..5b982035 100644 --- a/src/OpenIddict.Core/OpenIddictManager.cs +++ b/src/OpenIddict.Core/OpenIddictManager.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity; @@ -43,6 +44,12 @@ namespace OpenIddict { return Store.FindApplicationByLogoutRedirectUri(url, Context.RequestAborted); } + public virtual async Task 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 GetApplicationTypeAsync(TApplication application) { if (application == null) { throw new ArgumentNullException(nameof(application)); diff --git a/src/OpenIddict.Core/OpenIddictProvider.cs b/src/OpenIddict.Core/OpenIddictProvider.cs index e4a501b5..ff1a0419 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.cs +++ b/src/OpenIddict.Core/OpenIddictProvider.cs @@ -216,13 +216,16 @@ namespace OpenIddict { var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user)); - // Only add the name claim if the "profile" scope was present in the token request. - if (context.Request.GetScopes().Contains("profile", StringComparer.OrdinalIgnoreCase)) { + // Only add the name claim if the "profile" scope was present in the authorization request. + // 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"); } - // Only add the email address if the "email" scope was present in the token request. - if (context.Request.GetScopes().Contains("email", StringComparer.OrdinalIgnoreCase)) { + // Only add the email address if the "email" scope was present in the authorization request. + if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) { identity.AddClaim(ClaimTypes.Email, await manager.GetEmailAsync(user), destination: "id_token token"); } @@ -234,6 +237,60 @@ namespace OpenIddict { context.HandleResponse(); } + public override async Task ProfileEndpoint([NotNull] ProfileEndpointContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + 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) { var manager = context.HttpContext.RequestServices.GetRequiredService>(); @@ -282,14 +339,29 @@ namespace OpenIddict { var identity = new ClaimsIdentity(context.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 token request. - if (context.Request.GetScopes().Contains("profile", StringComparer.OrdinalIgnoreCase)) { - identity.AddClaim(ClaimTypes.Name, await manager.GetUserNameAsync(user), destination: "id_token token"); + if (context.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 (!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. - if (context.Request.GetScopes().Contains("email", StringComparer.OrdinalIgnoreCase)) { - identity.AddClaim(ClaimTypes.Email, await manager.GetEmailAsync(user), destination: "id_token token"); + if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) { + identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token"); } context.Validated(new ClaimsPrincipal(identity)); diff --git a/src/OpenIddict.Core/Views/Shared/Authorize.cshtml b/src/OpenIddict.Core/Views/Shared/Authorize.cshtml index 37454929..bb001f92 100644 --- a/src/OpenIddict.Core/Views/Shared/Authorize.cshtml +++ b/src/OpenIddict.Core/Views/Shared/Authorize.cshtml @@ -11,7 +11,11 @@
@Html.AntiForgeryToken() - + + @foreach (var parameter in Model.Item1.Parameters) { + + } +