Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenIddict v1.0.0-beta1-0481 - No longer supports OpenIddictUser #3

Closed
Shawski opened this issue Nov 18, 2016 · 7 comments
Closed

OpenIddict v1.0.0-beta1-0481 - No longer supports OpenIddictUser #3

Shawski opened this issue Nov 18, 2016 · 7 comments

Comments

@Shawski
Copy link

Shawski commented Nov 18, 2016

The latest build of OpenIddict does not have an OpenIddictUser object.

Reading up on this, it looks like it departs from the use of JWT.

See: capesean/openiddict-test#25

Especially this comment from one of the contributors:
"That said, the access token format is supposed to be completely opaque to clients, so if your JS library relies on the fact your access tokens are JWTs, then it's doing it completely wrong and you should consider using another one 😄"

Looks like we need a refactor of much of the authentication section in Chapters 8 & 9.

I bought this book to get an end-to-end sample project I can use a base of a new product. This OpenIddict issue is bit over my head right now.

@Darkseal
Copy link
Collaborator

Darkseal commented Nov 18, 2016

Hello and thanks for the feedback (and for purchasing the book as well).

Regarding your query, you can use OpenIddict with standard tokens without problems, or even ditch OpenIddict completely and implement your own token provider: the OpenGameList client app does not require tokens to be JWTs, nor it does make any attempt to read or decrypt it: as you can easily see, it just stores the token locally and send it back to the server by appending it on the Authorization header. As a matter of fact, they are opaque to the client (just as @PinpointTownes says).

Of course migrating from JWTs to standard tokens will require a minor refactor of Chapter 8 and 9, but this is quite easy to do as long as you understand the underlying logic and follow the OpenIddict implementation samples. That said, in case you need help with this, I can gladly help you with the transition from JWT to standard tokens.

As for the fact that the OpenIddictUser is not there anymore, I'm glad they now enforce the usage of the standard IdentityUser class, thus enforcing the default .NET Core Identity auth pattern.

@Darkseal
Copy link
Collaborator

Darkseal commented Nov 18, 2016

As a side note, I would like to clarify that (from the thread you mentioned) it doesn't look like OpenIddict is departing from using JWTs. Here's what happened there:

  • The OP asked for the missing OpenIddictUser identity class (which has been removed)
  • The project owner pointed him to standard code samples (which use the standard IdentityUser class instead, AND the standard tokens)
  • The OP asks for JWT samples, since the given samples don't use JWTs and he needs them to since he's decoding them with his client app.
  • The project owner replied that the given samples are viable regardless of token type, since it should be opaque to clients: he also warns the OP about having the client app rely to JWTs and/or decrypt JWTs, since it's not what you should do.

To keep it short, all we need to do is to use the given samples (with a grain of salt) and replace OpenIddictUser with IdentityUser, without worrying about JWTs.

@kevinchalet
Copy link

  • The OP asked for the missing OpenIddictUser class (which has been removed)
  • The project owner pointed him to standard code samples (which use standard tokens)
  • The OP asks for JWT samples
  • The project owner replied that the given samples are viable regardless of token type, since it should be - opaque to clients.

Yup, exactly that 😄

@Shawski
Copy link
Author

Shawski commented Nov 18, 2016

Thanks, Darkseal! I would love a link to an updated Chapter 10 project with the changes if that is possible.

@sgroulx2017
Copy link

Have you completed the refactoring of Chapter 8 and 9? We would for sure appreciate getting access to the new source code! Thanks

@ttchongtc
Copy link

After hours of effort, here's the working solution you can try if you run into the issue while following along this book:

Delete Migrations folder and run: dotnet ef database drop
Then run: dotnet ef migrations add "OpenIddict" -o "Data\Migrations"

Project.json:

{
"dependencies": {
"AspNet.Security.OAuth.Validation": "1.0.0-",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0",
"Microsoft.AspNetCore.Diagnostics": "1.1.0",
"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.1.0",
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
"Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Routing": "1.1.0",
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.AspNetCore.StaticFiles": "1.1.0",
"Microsoft.EntityFrameworkCore": "1.1.0",
"Microsoft.EntityFrameworkCore.Design": "1.1.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.1.0",
"Microsoft.EntityFrameworkCore.SqlServer.Design": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.1.0",
"Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.IdentityModel.Tokens": "5.1.3",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.1.0",
"Newtonsoft.Json": "9.0.1",
"OpenIddict": "1.0.0-
",
"OpenIddict.EntityFrameworkCore": "1.0.0-",
"OpenIddict.Mvc": "1.0.0-
",
"System.IdentityModel.Tokens.Jwt": "5.1.3",
"TinyMapper": "2.0.8"
},

"tools": {
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.1.0-preview4-final",
"Microsoft.EntityFrameworkCore.Tools.DotNet": "1.0.0-preview3-final"
},

"frameworks": {
"net461": {}
},

"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true
},

"publishOptions": {
"include": [
"wwwroot",
"Views",
"Areas/**/Views",
"appsettings.json",
"web.config"
]
},

"scripts": {
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
}
}

appsettings.json

{
"Authentication": {
"OpenIddict": {
"ApplicationId": "OpenGameList",
"DisplayName": "OpenGameList",
"AuthorizationEndPoint": "/api/connect/authorize",
"TokenEndPoint": "/api/connect/token",
"ClientId": "OpenGameList",
"ClientSecret": "1234567890_my_client_secret",
"Authority": "http://localhost:14600/"
}
},
"Data": {
"DefaultConnection": {
"ConnectionString": "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=OpenGameList;Integrated Security=True;MultipleActiveResultSets=True"
}
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"StaticFiles": {
"Headers": {
"Cache-Control": "no-cache, no-store",
"Pragma": "no-cache",
"Expires": "-1"
}
}
}

Startup.cs

using System;
using System.IdentityModel.Tokens.Jwt;
using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using OpenGameListWebApp.Data;
using Nelibur.ObjectMapper;
using OpenGameListWebApp.Data.Items;
using OpenGameListWebApp.Data.Users;
using OpenGameListWebApp.ViewModels;
using OpenIddict.Core;
using OpenIddict.Models;

namespace OpenGameList
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}

    public IConfigurationRoot Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Add a reference to the Configuration object for DI
        services.AddSingleton<IConfiguration>(c => Configuration);

        // Add framework services.
        services.AddMvc();

        // Add ApplicationDbContext.
        services.AddDbContext<ApplicationDbContext>(options =>
        {
            options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);

            // Register the entity sets needed by OpenIddict.
            // Note: use the generic overload if you need
            // to replace the default OpenIddict entities.
            options.UseOpenIddict();
        });

        // Add EntityFramework's Identity support.
        //services.AddEntityFramework();

        // Add Identity Services & Stores
        services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            {
                options.User.RequireUniqueEmail = true;
                options.Password.RequireNonAlphanumeric = false;
                options.Cookies.ApplicationCookie.AutomaticChallenge = false;
            })
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // 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 = OpenIdConnectConstants.Claims.Name;
            options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
            options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
        });

        // Register OpenIddict services, including the default Entity Framework stores.
        services.AddOpenIddict(options =>
        {
            // Register the ASP.NET Core MVC binder used by OpenIddict.
            // Note: if you don't call this method, you won't be able to bind OpenIdConnectRequest
            // or OpenIdConnectResponse parameters.
            options.AddMvcBinders();

            // Registers the Entity Framework stores.
            options.AddEntityFrameworkCoreStores<ApplicationDbContext>();

            // Use JWT access tokens instead of the default encrypted format
            options.UseJsonWebTokens();

            // Set custom token endpoint. Default is /connect/token
            options.EnableTokenEndpoint(Configuration["Authentication:OpenIddict:TokenEndPoint"]);

            // Set a custom auth endpoint (default is /connect/authorize)
            options.EnableAuthorizationEndpoint(Configuration["Authentication:OpenIddict:AuthorizationEndPoint"]);

            // Allow client applications to use the grant_type = password flow
            options.AllowPasswordFlow();

            // Enable support for both authorization & implicit flows
            options.AllowAuthorizationCodeFlow();

            options.AllowImplicitFlow();

            // Allow the client to refresh tokens
            options.AllowRefreshTokenFlow();

            // During development, you can disable the HTTPS requirement
            options.DisableHttpsRequirement();

            // Register a new ephemeral key for development.
            // We will register a X.509 certificate in production.
            options.AddEphemeralSigningKey();
        });

        // Add ApplicationDbContext's DbSeeder
        services.AddSingleton<DbSeeder>();
        services.AddSingleton<OpenIddictApplicationManager<OpenIddictApplication>>();
        services.AddSingleton < SignInManager<ApplicationUser>>();
        services.AddSingleton<UserManager<ApplicationUser>>();
        services.AddOptions();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, DbSeeder dbSeeder)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        // Configure a rewrite rule to auto-lookup for standard default files such as index.html.
        app.UseDefaultFiles();

        // Serve static files (html, css, js, images & more). See also the following URL:
        // https://docs.asp.net/en/latest/fundamentals/static-files.html for further reference.
        app.UseStaticFiles(new StaticFileOptions()
        {
            OnPrepareResponse = (context) =>
            {
                // Disable caching for all static files.
                context.Context.Response.Headers["Cache-Control"] = Configuration["StaticFiles:Headers:Cache-Control"];
                context.Context.Response.Headers["Pragma"] = Configuration["StaticFiles:Headers:Pragma"];
                context.Context.Response.Headers["Expires"] = Configuration["StaticFiles:Headers:Expires"];
            }
        });

        app.UseIdentity();
        app.UseOAuthValidation();

        // Add a custom Jwt Provider to generate Tokens
        //app.UseJwtProvider();

        // Add OpenIddict middleware
        // Note: UseOpenIddict() must be registered after app.UseIdentity() and the external social providers
        app.UseOpenIddict();

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

        // Add the Jwt Bearer Header Authentication to validate Tokens
        app.UseJwtBearerAuthentication(new JwtBearerOptions
        {
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
            RequireHttpsMetadata = false,
            // Allows the JWT bearer middleware to download the signing key
            Authority = Configuration["Authentication:OpenIddict:Authority"],
            Audience = "resource_server",
            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = OpenIdConnectConstants.Claims.Name,
                RoleClaimType = OpenIdConnectConstants.Claims.Role
            }
        });

        // Add MVC to the pipeline
        app.UseMvc();

        // TinyMapper binding configuration
        TinyMapper.Bind<Item, ItemViewModel>();

        // Seed the Database (if needed)
        try
        {
            dbSeeder.SeedAsync().Wait();
        }
        catch (AggregateException e)
        {
            throw new Exception(e.ToString());
        }
    }
}

}

ApplicationDbContext.cs

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using OpenGameListWebApp.Data.Comments;
using OpenGameListWebApp.Data.Items;
using OpenGameListWebApp.Data.Users;
using OpenIddict.Models;

namespace OpenGameListWebApp.Data
{
public class ApplicationDbContext : IdentityDbContext
{
#region Constructor

    public ApplicationDbContext(DbContextOptions options): base(options)
    {
    }
    #endregion Constructor

    #region Methods

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>().ToTable("Users");
        modelBuilder.Entity<ApplicationUser>()
            .HasMany(u => u.Items)
            .WithOne(i => i.Author);
        modelBuilder.Entity<ApplicationUser>()
            .HasMany(u => u.Comments)
            .WithOne(c => c.Author)
            .HasPrincipalKey(u => u.Id);

        modelBuilder.Entity<Item>().ToTable("Items");
        modelBuilder.Entity<Item>().Property(i => i.Id).ValueGeneratedOnAdd();
        modelBuilder.Entity<Item>().HasOne(i => i.Author).WithMany(u => u.Items);
        modelBuilder.Entity<Item>().HasMany(i => i.Comments).WithOne(c => c.Item);

        modelBuilder.Entity<Comment>().ToTable("Comments");
        modelBuilder.Entity<Comment>()
            .HasOne(c => c.Author)
            .WithMany(u => u.Comments)
            .HasForeignKey(c => c.UserId)
            .OnDelete(DeleteBehavior.Restrict);
        modelBuilder.Entity<Comment>().HasOne(c => c.Item).WithMany(i => i.Comments);
        modelBuilder.Entity<Comment>().HasOne(c => c.Parent).WithMany(c => c.Children);
        modelBuilder.Entity<Comment>().HasMany(c => c.Children).WithOne(c => c.Parent);
    }
    #endregion

    #region Properties
    public DbSet<Item> Items { get; set; }
    public DbSet<Comment> Comments { get; set; }
    public DbSet<OpenIddictApplication> Applications { get; set; }

    #endregion Properties
}

}

Make sure to have below in DbSeeder.cs

    public async Task SeedAsync()
    {
        // Create the Db if it doesn’t exist
        DbContext.Database.EnsureCreated();
        // Create default Application
        if (!DbContext.Applications.Any()) CreateApplication();
        // Create default Users
        if (await DbContext.Users.CountAsync() == 0) await CreateUsersAsync();
        // Create default Items (if there are none) and Comments
        if (await DbContext.Items.CountAsync() == 0) CreateItems();
    }
    #endregion Public Methods

    #region Seed Methods
    private void CreateApplication()
    {
        DbContext.Applications.Add(new OpenIddictApplication
        {
            Id = Configuration["Authentication:OpenIddict:ApplicationId"],
            DisplayName = Configuration["Authentication:OpenIddict:DisplayName"],
            RedirectUri = Configuration["Authentication:OpenIddict:TokenEndPoint"],
            LogoutRedirectUri = "/",
            ClientId = Configuration["Authentication:OpenIddict:ClientId"],
            ClientSecret = Crypto.HashPassword(Configuration["Authentication:OpenIddict:ClientSecret"]),
            Type = OpenIddictConstants.ClientTypes.Public
        });
        DbContext.SaveChanges();
    }

Then add ConnectController.cs

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using OpenGameListWebApp.Data.Users;
using OpenGameListWebApp.ViewModels;
using OpenIddict.Core;
using OpenIddict.Models;

namespace OpenGameListWebApp.Controllers
{
[Route("api/[controller]")]
public class ConnectController : Controller
{
private readonly OpenIddictApplicationManager _applicationManager;
private readonly IOptions _identityOptions;
private readonly SignInManager _signInManager;
private readonly UserManager _userManager;

    public ConnectController(
        OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
        IOptions<IdentityOptions> identityOptions,
        SignInManager<ApplicationUser> signInManager,
        UserManager<ApplicationUser> userManager)
    {
        _applicationManager = applicationManager;
        _identityOptions = identityOptions;
        _signInManager = signInManager;
        _userManager = userManager;
    }

    // Note: to support interactive flows like the code flow,
    // you must provide your own authorization endpoint action:
    [Authorize, HttpGet("authorize")]
    public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
    {
        Debug.Assert(request.IsAuthorizationRequest(),
            "The OpenIddict binder for ASP.NET Core MVC is not registered. " +
            "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");

        // Retrieve the application details from the database.
        var application = await _applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted);
        if (application == null)
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.InvalidClient,
                ErrorDescription = "Details concerning the calling client application cannot be found in the database."
            });
        }

        // Flow the request_id to allow OpenIddict to restore
        // the original authorization request from the cache.
        return new JsonResult(new AuthorizeViewModel
        {
            ApplicationName = application.DisplayName,
            RequestId = request.RequestId,
            Scope = request.Scope
        }, DefaultJsonSettings);
    }

    [Authorize]
    [HttpPost("authorize"), ValidateAntiForgeryToken]
    public async Task<IActionResult> Accept(OpenIdConnectRequest request)
    {
        Debug.Assert(request.IsAuthorizationRequest(),
            "The OpenIddict binder for ASP.NET Core MVC is not registered. " +
            "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");

        // Retrieve the profile of the logged in user.
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.ServerError,
                ErrorDescription = "An internal error has occurred"
            });
        }

        // Create a new authentication ticket.
        var ticket = await CreateTicketAsync(request, user);

        // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
        return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
    }

    [HttpPost("logout"), ValidateAntiForgeryToken]
    public async Task<IActionResult> Logout()
    {
        // Ask ASP.NET Core Identity to delete the local and external cookies created
        // when the user agent is redirected from the external identity provider
        // after a successful authentication flow (e.g Google or Facebook).
        await _signInManager.SignOutAsync();

        // Returning a SignOutResult will ask OpenIddict to redirect the user agent
        // to the post_logout_redirect_uri specified by the client application.
        return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme);
    }

    [HttpPost("token"), Produces("application/json")]
    public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
    {
        Debug.Assert(request.IsTokenRequest(),
            "The OpenIddict binder for ASP.NET Core MVC is not registered. " +
            "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");

        if (request.IsPasswordGrantType())
        {
            var user = await _userManager.FindByNameAsync(request.Username);
            if (user == null)
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The username/password couple is invalid."
                });
            }

            // Ensure the user is allowed to sign in.
            if (!await _signInManager.CanSignInAsync(user))
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The specified user is not allowed to sign in."
                });
            }

            // Reject the token request if two-factor authentication has been enabled by the user.
            if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The specified user is not allowed to sign in."
                });
            }

            // Ensure the user is not already locked out.
            if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The username/password couple is invalid."
                });
            }

            // Ensure the password is valid.
            if (!await _userManager.CheckPasswordAsync(user, request.Password))
            {
                if (_userManager.SupportsUserLockout)
                {
                    await _userManager.AccessFailedAsync(user);
                }

                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The username/password couple is invalid."
                });
            }

            if (_userManager.SupportsUserLockout)
            {
                await _userManager.ResetAccessFailedCountAsync(user);
            }

            // Create a new authentication ticket.
            var ticket = await CreateTicketAsync(request, user);

            return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
        }
        else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
        {
            // Retrieve the claims principal stored in the authorization code/refresh token.
            var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
                OpenIdConnectServerDefaults.AuthenticationScheme);

            // 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(info.Principal);
            if (user == null)
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The token is no longer valid."
                });
            }

            // Ensure the user is still allowed to sign in.
            if (!await _signInManager.CanSignInAsync(user))
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The user is no longer allowed to sign in."
                });
            }

            // Create a new authentication ticket, but reuse the properties stored in the
            // authorization code/refresh token, including the scopes originally granted.
            var ticket = await CreateTicketAsync(request, user, info.Properties);

            return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
        }

        return BadRequest(new OpenIdConnectResponse
        {
            Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
            ErrorDescription = "The specified grant type is not supported."
        });
    }

    private async Task<AuthenticationTicket> CreateTicketAsync(
        OpenIdConnectRequest request,
        ApplicationUser user,
        AuthenticationProperties properties = null)
    {
        // Create a new ClaimsPrincipal containing the claims that
        // will be used to create an id_token, a token or a code.
        var principal = await _signInManager.CreateUserPrincipalAsync(user);

        // Create a new authentication ticket holding the user identity.
        var ticket = new AuthenticationTicket(principal, properties,
            OpenIdConnectServerDefaults.AuthenticationScheme);

        if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType())
        {
            // Set the list of scopes granted to the client application.
            // Note: the offline_access scope must be granted
            // to allow OpenIddict to return a refresh token.
            ticket.SetScopes(new[]
            {
                OpenIdConnectConstants.Scopes.OpenId,
                OpenIdConnectConstants.Scopes.Email,
                OpenIdConnectConstants.Scopes.Profile,
                OpenIdConnectConstants.Scopes.OfflineAccess,
                OpenIddictConstants.Scopes.Roles
            }.Intersect(request.GetScopes()));
        }

        ticket.SetResources("resource_server");

        // 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.
        foreach (var claim in ticket.Principal.Claims)
        {
            // Never include the security stamp in the access and identity tokens, as it's a secret value.
            if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
            {
                continue;
            }

            // Always include the user identifier in the
            // access token and the identity token.
            if (claim.Type == OpenIdConnectConstants.Claims.Name)
            {
                claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
                                      OpenIdConnectConstants.Destinations.IdentityToken);
            }
            // Include the role claims, but only if the "roles" scope was requested.
            else if (claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Scopes.Roles))
            {
                claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
                                      OpenIdConnectConstants.Destinations.IdentityToken);
            }

            var destinations = new List<string>
            {
                OpenIdConnectConstants.Destinations.AccessToken
            };

            // Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
            // The other claims will only be added to the access_token, which is encrypted when using the default format.
            if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
                (claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
                (claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
            {
                destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
            }

            claim.SetDestinations(destinations);
        }

        return ticket;
    }

    /// <summary>
    /// Returns a suitable JsonSerializerSettings object that can be used to generate the JsonResult return value for this Controller's methods.
    /// </summary>
    private JsonSerializerSettings DefaultJsonSettings => new JsonSerializerSettings()
    {
        Formatting = Formatting.Indented
    };
}

}

ApplicationUser.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using OpenGameListWebApp.Data.Comments;
using OpenGameListWebApp.Data.Items;

namespace OpenGameListWebApp.Data.Users
{
public class ApplicationUser : IdentityUser
{
#region Constructor
public ApplicationUser()
{

    }
    #endregion

    #region Related Properties
    /// <summary>
    /// A list of items written by this user: this property will be loaded on first use using EF's lazy loading feature.
    /// </summary>
    public virtual List<Item> Items { get; set; }

    /// <summary>
    /// A list of comments written by this user: this property will be loaded on first use using EF's lazy loading feature.
    /// </summary>
    public virtual List<Comment> Comments { get; set; }
    #endregion Related Properties

    #region Properties

    public string DisplayName { get; set; }

    public string Notes { get; set; }

    [Required]
    public int Type { get; set; }

    [Required]
    public int Flags { get; set; }

    [Required]
    public DateTime CreatedDate { get; set; }

    [Required]
    public DateTime LastModifiedDate { get; set; }

    #endregion Properties
}

}

That should do it.

@Darkseal
Copy link
Collaborator

Darkseal commented Apr 8, 2017

@ttchongtc , kudos and many thanks for the great work!!!

@Darkseal Darkseal closed this as completed Apr 8, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

5 participants