diff --git a/samples/Mvc.Client/Controllers/HomeController.cs b/samples/Mvc.Client/Controllers/HomeController.cs index 9bc078db..4c9b616c 100644 --- a/samples/Mvc.Client/Controllers/HomeController.cs +++ b/samples/Mvc.Client/Controllers/HomeController.cs @@ -6,9 +6,16 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Mvc.Client.Controllers { public class HomeController : Controller { + private readonly HttpClient _client; + + public HomeController(HttpClient client) { + _client = client; + } + [HttpGet("~/")] public ActionResult Index() { return View("Home"); @@ -16,21 +23,19 @@ namespace Mvc.Client.Controllers { [Authorize, HttpPost("~/")] public async Task Index(CancellationToken cancellationToken) { - using (var client = new HttpClient()) { - var token = await HttpContext.Authentication.GetTokenAsync("access_token"); - if (string.IsNullOrEmpty(token)) { - throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " + - "Make sure that SaveTokens is set to true in the OIDC options."); - } + var token = await HttpContext.Authentication.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); + if (string.IsNullOrEmpty(token)) { + throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " + + "Make sure that SaveTokens is set to true in the OIDC options."); + } - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:54540/api/message"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:54540/api/message"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - var response = await client.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + var response = await _client.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); - return View("Home", model: await response.Content.ReadAsStringAsync()); - } + return View("Home", model: await response.Content.ReadAsStringAsync()); } } } \ No newline at end of file diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 8ad3992d..71c46d00 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; @@ -13,6 +14,8 @@ namespace Mvc.Client { }); services.AddMvc(); + + services.AddSingleton(); } public void Configure(IApplicationBuilder app) { diff --git a/samples/Mvc.Server/Controllers/AccountController.cs b/samples/Mvc.Server/Controllers/AccountController.cs index 54fe77f7..5186942c 100644 --- a/samples/Mvc.Server/Controllers/AccountController.cs +++ b/samples/Mvc.Server/Controllers/AccountController.cs @@ -1,8 +1,6 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using AspNet.Security.OAuth.Validation; -using AspNet.Security.OpenIdConnect.Primitives; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -10,8 +8,6 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Mvc.Server.Models; using Mvc.Server.Services; using Mvc.Server.ViewModels.Account; -using Newtonsoft.Json.Linq; -using OpenIddict.Core; namespace Mvc.Server.Controllers { [Authorize] @@ -36,44 +32,6 @@ namespace Mvc.Server.Controllers { _applicationDbContext = applicationDbContext; } - // - // GET: /Account/Userinfo - [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)] - [HttpGet, Produces("application/json")] - public async Task Userinfo() { - var user = await _userManager.GetUserAsync(User); - if (user == null) { - return BadRequest(new OpenIdConnectResponse { - Error = OpenIdConnectConstants.Errors.InvalidGrant, - ErrorDescription = "The user profile is no longer available." - }); - } - - var claims = new JObject(); - - // Note: the "sub" claim is a mandatory claim and must be included in the JSON response. - claims[OpenIdConnectConstants.Claims.Subject] = user.Id.ToString(); - - if (User.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIdConnectConstants.Scopes.Email)) { - claims[OpenIdConnectConstants.Claims.Email] = user.Email; - claims[OpenIdConnectConstants.Claims.EmailVerified] = user.EmailConfirmed; - } - - if (User.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIdConnectConstants.Scopes.Phone)) { - claims[OpenIdConnectConstants.Claims.PhoneNumber] = user.PhoneNumber; - claims[OpenIdConnectConstants.Claims.PhoneNumberVerified] = user.PhoneNumberConfirmed; - } - - if (User.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIddictConstants.Scopes.Roles)) { - claims[OpenIddictConstants.Claims.Roles] = JArray.FromObject(await _userManager.GetRolesAsync(user)); - } - - // Note: the complete list of standard claims supported by the OpenID Connect specification - // can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - - return Json(claims); - } - // // GET: /Account/Login [HttpGet] diff --git a/samples/Mvc.Server/Controllers/UserinfoController.cs b/samples/Mvc.Server/Controllers/UserinfoController.cs new file mode 100644 index 00000000..cb7d41ac --- /dev/null +++ b/samples/Mvc.Server/Controllers/UserinfoController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using AspNet.Security.OAuth.Validation; +using AspNet.Security.OpenIdConnect.Primitives; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Mvc.Server.Models; +using Newtonsoft.Json.Linq; +using OpenIddict.Core; + +namespace Mvc.Server.Controllers { + [Route("api")] + public class UserinfoController : Controller { + private readonly UserManager _userManager; + + public UserinfoController(UserManager userManager) { + _userManager = userManager; + } + + // + // GET: /api/userinfo + [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)] + [HttpGet("userinfo"), Produces("application/json")] + public async Task Userinfo() { + var user = await _userManager.GetUserAsync(User); + if (user == null) { + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The user profile is no longer available." + }); + } + + var claims = new JObject(); + + // Note: the "sub" claim is a mandatory claim and must be included in the JSON response. + claims[OpenIdConnectConstants.Claims.Subject] = await _userManager.GetUserIdAsync(user); + + if (User.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIdConnectConstants.Scopes.Email)) { + claims[OpenIdConnectConstants.Claims.Email] = await _userManager.GetEmailAsync(user); + claims[OpenIdConnectConstants.Claims.EmailVerified] = await _userManager.IsEmailConfirmedAsync(user); + } + + if (User.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIdConnectConstants.Scopes.Phone)) { + claims[OpenIdConnectConstants.Claims.PhoneNumber] = await _userManager.GetPhoneNumberAsync(user); + claims[OpenIdConnectConstants.Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user); + } + + if (User.HasClaim(OpenIdConnectConstants.Claims.Scope, OpenIddictConstants.Scopes.Roles)) { + claims[OpenIddictConstants.Claims.Roles] = JArray.FromObject(await _userManager.GetRolesAsync(user)); + } + + // Note: the complete list of standard claims supported by the OpenID Connect specification + // can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + + return Json(claims); + } + } +} diff --git a/samples/Mvc.Server/Extensions/AppBuilderExtensions.cs b/samples/Mvc.Server/Extensions/AppBuilderExtensions.cs new file mode 100644 index 00000000..36bbca3d --- /dev/null +++ b/samples/Mvc.Server/Extensions/AppBuilderExtensions.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +namespace Mvc.Server.Extensions { + public static class AppBuilderExtensions { + public static IApplicationBuilder UseWhen(this IApplicationBuilder app, + Func condition, Action configuration) { + if (app == null) { + throw new ArgumentNullException(nameof(app)); + } + + if (condition == null) { + throw new ArgumentNullException(nameof(condition)); + } + + if (configuration == null) { + throw new ArgumentNullException(nameof(configuration)); + } + + var builder = app.New(); + configuration(builder); + + return app.Use(next => { + builder.Run(next); + + var branch = builder.Build(); + + return context => { + if (condition(context)) { + return branch(context); + } + + return next(context); + }; + }); + } + } +} diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 24a2f719..a9fed92f 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Mvc.Server.Extensions; using Mvc.Server.Models; using Mvc.Server.Services; using OpenIddict.Core; @@ -50,7 +51,7 @@ namespace Mvc.Server { .EnableAuthorizationEndpoint("/connect/authorize") .EnableLogoutEndpoint("/connect/logout") .EnableTokenEndpoint("/connect/token") - .EnableUserinfoEndpoint("/Account/Userinfo") + .EnableUserinfoEndpoint("/api/userinfo") // Note: the Mvc.Client sample only uses the code flow and the password flow, but you // can enable the other flows if you need to support implicit or client credentials. @@ -101,36 +102,40 @@ namespace Mvc.Server { app.UseStaticFiles(); - // Add a middleware used to validate access - // tokens and protect the API endpoints. - app.UseOAuthValidation(); + app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), branch => { + // Add a middleware used to validate access + // tokens and protect the API endpoints. + branch.UseOAuthValidation(); - // Alternatively, you can also use the introspection middleware. - // Using it is recommended if your resource server is in a - // different application/separated from the authorization server. - // - // app.UseOAuthIntrospection(options => { - // options.AutomaticAuthenticate = true; - // options.AutomaticChallenge = true; - // options.Authority = "http://localhost:54540/"; - // options.Audience = "resource_server"; - // options.ClientId = "resource_server"; - // options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd"; - // }); - - app.UseIdentity(); - - app.UseGoogleAuthentication(new GoogleOptions { - ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com", - ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f" + // Alternatively, you can also use the introspection middleware. + // Using it is recommended if your resource server is in a + // different application/separated from the authorization server. + // + // branch.UseOAuthIntrospection(options => { + // options.AutomaticAuthenticate = true; + // options.AutomaticChallenge = true; + // options.Authority = "http://localhost:54540/"; + // options.Audiences.Add("resource_server"); + // options.ClientId = "resource_server"; + // options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd"; + // }); }); - app.UseTwitterAuthentication(new TwitterOptions { - ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g", - ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI" - }); + app.UseWhen(context => !context.Request.Path.StartsWithSegments("/api"), branch => { + branch.UseStatusCodePagesWithReExecute("/error"); - app.UseStatusCodePagesWithReExecute("/error"); + branch.UseIdentity(); + + branch.UseGoogleAuthentication(new GoogleOptions { + ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com", + ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f" + }); + + branch.UseTwitterAuthentication(new TwitterOptions { + ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g", + ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI" + }); + }); app.UseOpenIddict();