Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Gunnar Már Óttarsson 11 posts 47 karma points
    Jul 18, 2022 @ 10:45
    Gunnar Már Óttarsson
    3

    Hacked together an Umbraco 10 and OpenIddict OAuth implementation

    When porting our solution from U8 to U10 we had consumers relying on endpoints secured via OAuth jwt bearer tokens.

    We had been previously using umbraco-authu but I was hoping to be able to migrate to OpenIddict for aspnet core.

    I set up the client credentials flow using the samples on the OpenIddict site

    and customized my auth controller like so (this is mostly the code from the sample, the GetDestinations method was copied verbatim from sample)

    public class AuthorizationController : Controller
    {
        private readonly IOpenIddictApplicationManager _applicationManager;
        private readonly IOpenIddictScopeManager _scopeManager;
        private readonly Umbraco.Cms.Web.Common.Security.IMemberSignInManager _signInManager;
        private readonly IMemberManager _memberManager;
    
        public AuthorizationController(
            IOpenIddictApplicationManager applicationManager,
            IOpenIddictScopeManager scopeManager,
            IMemberSignInManager signInManager,
            IMemberManager memberManager)
        {
            _applicationManager = applicationManager;
            _scopeManager = scopeManager;
            _signInManager = signInManager;
            _memberManager = memberManager;
        }
    
        [HttpPost("~/connect/token"), Produces("application/json")]
        public async Task<IActionResult> Exchange()
        {
            var request = HttpContext.GetOpenIddictServerRequest();
            if (request.IsClientCredentialsGrantType())
            {
                Microsoft.AspNetCore.Identity.SignInResult result = await _signInManager.PasswordSignInAsync(
                    request.Username, request.Password, false, true);
    
                if (result.Succeeded)
                {
                    var m = await _memberManager.GetCurrentMemberAsync();
    
                    // Create a new ClaimsIdentity containing the claims that
                    // will be used to create an id_token, a token or a code.
                    var identity = new ClaimsIdentity(
                        TokenValidationParameters.DefaultAuthenticationType,
                        Claims.Name, Claims.Role);
    
                    identity.AddClaim(Claims.Subject, request.Username,
                        Destinations.AccessToken, Destinations.IdentityToken);
    
                    identity.AddClaim(Claims.Name, m.Name,
                        Destinations.AccessToken, Destinations.IdentityToken);
    
                    // Note: In the original OAuth 2.0 specification, the client credentials grant
                    // doesn't return an identity token, which is an OpenID Connect concept.
                    //
                    // As a non-standardized extension, OpenIddict allows returning an id_token
                    // to convey information about the client application when the "openid" scope
                    // is granted (i.e specified when calling principal.SetScopes()). When the "openid"
                    // scope is not explicitly set, no identity token is returned to the client application.
    
                    // Set the list of scopes granted to the client application in access_token.
                    var principal = new ClaimsPrincipal(identity);
                    principal.SetScopes(request.GetScopes());
                    principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
    
                    foreach (var claim in principal.Claims)
                    {
                        claim.SetDestinations(GetDestinations(claim));
                    }
    
                    return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
                }
    
    
                //if (result.RequiresTwoFactor)
                //{
                //    MemberIdentityUser attemptedUser = await _memberManager.FindByNameAsync(model.Username);
                //    if (attemptedUser == null!)
                //    {
                //        return new ValidationErrorResult(
                //            $"No local member found for username {model.Username}");
                //    }
    
                //    IEnumerable<string> providerNames =
                //        await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key);
                //    ViewData.SetTwoFactorProviderNames(providerNames);
                //}
                else if (result.IsLockedOut)
                {
                    throw new InvalidOperationException("Member is locked out");
                }
                else if (result.IsNotAllowed)
                {
                    throw new InvalidOperationException("Member is not allowed");
                }
                else
                {
                    throw new ArgumentException("Invalid username or password");
                }
            }
    
            throw new NotImplementedException("The specified grant type is not implemented.");
        }
    }
    

    This code returns a token when /connect/token is called like so.

    curl -XPOST -d 'grant_type=client_credentials&[email protected]&password=password&client_id=api&client_secret=aa073e48-d3f0-464d-8068-7f8182c0f326' 'https://localhost:44317/connect/token'
    

    The samples show how to seed openiddict with a client in the Worker.cs file.

    and when using the default openiddict scheme the http context user principal is populated with claims. However calls to f.x. IMemberManager GetCurrentMemberAsync return null.

    My fix was to create a custom authentication scheme with the following handler

    public class UmbracoOAuthOptions : OpenIddictValidationAspNetCoreOptions
    {
    
    }
    
    public class UmbracoOAuthHandler : OpenIddictValidationAspNetCoreHandler
    {
        readonly IMemberService _memberService;
        readonly IMemberManager _memberManager;
        readonly IMemberSignInManager _memberSignInManager;
        public UmbracoOAuthHandler(
            IOpenIddictValidationDispatcher dispatcher,
            IOpenIddictValidationFactory factory,
            IOptionsMonitor<UmbracoOAuthOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IMemberSignInManager memberSignInManager,
            IMemberService memberService,
            IMemberManager memberManager)
            : base(dispatcher, factory, options, logger, encoder, clock)
        {
            _memberSignInManager = memberSignInManager;
            _memberService = memberService;
            _memberManager = memberManager;
        }
    
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            var authResult = await base.HandleAuthenticateAsync();
    
            if (authResult.Succeeded)
            {
            // This magic somehow fixes/populates the session with umbraco member identity
            // This same call does not work from the controller, unless we already called it from this handler
            var curMemb = await _memberManager.GetCurrentMemberAsync();
            }
    
            return authResult;
        }
    }
    

    and then registering the scheme in startup

    services.Configure<Microsoft.AspNetCore.Authentication.AuthenticationOptions>(config =>
    {
          config.AddScheme<UmbracoOAuthHandler>(GlobalSettings.OAuthScheme, displayName: null);
    });
    

    While this works, it's hacky and obviously I have no idea what I'm doing. Would love to get some feedback from anyone who's knowledgeable on the subject ! :) Specifically I'm unclear on why I need to call the same method in IMemberManager from the auth handler before calling it in controller actions

Please Sign in or register to post replies

Write your reply to:

Draft