openiddict-core icon indicating copy to clipboard operation
openiddict-core copied to clipboard

Implement device grant type in Blazor server

Open Saccu19 opened this issue 2 weeks ago • 5 comments

Confirm you've already contributed to this project or that you sponsor it

  • [x] I confirm I'm a sponsor or a contributor

Version

7.2.0

Question

Hello, we have reviewed the samples for the Device Grant type and found them very useful. However, we are looking for a solution to implement directly in a Blazor Server application within the Razor pages, we are particularly interested in managing the verification and the revocation (authorizations and tokens) directly within the Razor pages.

Saccu19 avatar Dec 04 '25 11:12 Saccu19

Hi,

Support is reserved to sponsors and contributors. For more information on how to sponsor the project on GitHub, visit https://github.com/sponsors/kevinchalet.

Hope to see you on board soon!

kevinchalet avatar Dec 04 '25 11:12 kevinchalet

Hi, I made a mistake. I'm part of DevQ Srl, which are contributors. Do I have to recreate the issue?

Il giorno gio 4 dic 2025 alle 12:42 Kévin Chalet @.***> ha scritto:

kevinchalet left a comment (openiddict/openiddict-core#2402) https://github.com/openiddict/openiddict-core/issues/2402#issuecomment-3611762969

Hi,

Support is reserved to sponsors and contributors. For more information on how to sponsor the project on GitHub, visit https://github.com/sponsors/kevinchalet.

Hope to see you on board soon!

— Reply to this email directly, view it on GitHub https://github.com/openiddict/openiddict-core/issues/2402#issuecomment-3611762969, or unsubscribe https://github.com/notifications/unsubscribe-auth/A7YQO76F2JNOM62WZCS6VD34AAM2ZAVCNFSM6AAAAACOA2D2DKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTMMJRG43DEOJWHE . You are receiving this because you authored the thread.Message ID: @.***>

Saccu19 avatar Dec 04 '25 11:12 Saccu19

Ah, that changes things drastically 🤣

However, we are looking for a solution to implement directly in a Blazor Server application within the Razor pages, we are particularly interested in managing the verification and the revocation (authorizations and tokens) directly within the Razor pages.

Token generation and validation are always handled by OpenIddict itself - it's just too sensitive to leave them as an exercise to users - so the user code (which is the only "token" sent as part of the end-user verification - either contained in the URI or typed by the user) will always be validated by OpenIddict. Similarly, during the token request, the device code will be validated by OpenIddict itself, just like it does for every other flow.

Can you tell me more about your scenario?

Note: implementing the end-user verification page using Blazor Server shouldn't be too complicated: the only difference with the same app is how you'll handle the POST part (where the user will approve or not the authorization demand). Unlike the equivalent for an interactive flow (e.g code flow), the HTTP response returned by the end-user verification isn't essential (it doesn't contain any token), so you can technically do it as part of a headless XMLHttpRequest call if you prefer.

kevinchalet avatar Dec 04 '25 12:12 kevinchalet

Here is some additional context about our scenario.

Our Blazor Server application allows a user to associate multiple devices. When a user creates a device, in addition to providing the required data, they also need to enter the user_code. When the device creation form is submitted, the “verify” phase of the Device Flow should be triggered. What we are trying to achieve is executing this phase directly inside a .razor page. We do not want to change the behavior of OpenIddict; rather, we want to move the logic from a controller into a Razor component.

We are following this approach because we need to associate the unique device ID with the “sub” claim. This allows us to retrieve the “sub” from the claims and identify the associated device.

The problem is that after moving the logic of the “verify” POST step into a .razor page, we don’t know how to handle the SignIn() call.

Saccu19 avatar Dec 04 '25 14:12 Saccu19

The problem is that after moving the logic of the “verify” POST step into a .razor page, we don’t know how to handle the SignIn() call.

Unlike MVC controllers or minimal API actions, Razor Components don't have an equivalent of the SignInResult/ForbidResult helpers, so you must trigger the call to IAuthenticationService.SignInAsync() or IAuthenticationService.ForbidAsync() manually. You can directly resolve IAuthenticationService yourself or use the HttpContext extensions.

I'm by no means a Blazor expert (never been a huge fan to be honest), but here's a - crude but working - Blazor Server sample that should put you on the right rack:

@page "/Connect/Verify"

@using System.Security.Claims
@using YourNamespace.Data
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@using Microsoft.Extensions.Primitives
@using Microsoft.IdentityModel.Tokens
@using OpenIddict.Abstractions
@using OpenIddict.Server.AspNetCore
@using static OpenIddict.Abstractions.OpenIddictConstants

@inject IOpenIddictApplicationManager ApplicationManager
@inject IOpenIddictScopeManager ScopeManager
@inject UserManager<ApplicationUser> UserManager

@attribute [Authorize]

<PageTitle>Authorization</PageTitle>

<div class="jumbotron">
    <h1>Authorization</h1>

    @if (string.IsNullOrEmpty(userCode) || !string.IsNullOrEmpty(errorMessage))
    {
        @if (!string.IsNullOrEmpty(errorMessage))
        {
            <p class="lead text-center alert alert-warning">@errorMessage</p>
        }

        <p class="lead text-left">Enter the user code given by the client application:</p>

        <form action="Connect/Verify" method="get">
            <div class="form-check">
                <input class="form-control" name="user_code" type="text" />
            </div>

            <input class="btn btn-lg btn-success" type="submit" value="Submit" />
        </form>

        <form @formname="approve-authorization"></form>
    }
    else
    {
        <p class="lead text-left">Do you want to grant <strong>@applicationName</strong> access to your data?</p>
        <p class="lead text-center alert alert-warning">
            Make sure that the code displayed on the device is <strong>@userCode</strong>.
            <br />
            If the two codes don't match, press "No" to reject the authorization demand.
        </p>

        <form @formname="approve-authorization" action="Connect/Verify" @onsubmit="OnSubmitAsync" method="post">
            <AntiforgeryToken />

            @foreach (var parameter in HttpContext.Request.HasFormContentType ?
                    (IEnumerable<KeyValuePair<string, StringValues>>) HttpContext.Request.Form : HttpContext.Request.Query)
            {
                <input type="hidden" name="@parameter.Key" value="@parameter.Value" />
            }

            <div>
                <button type="submit" name="Action" value="accept" class="btn btn-lg btn-success">Yes</button>
                <button type="submit" name="Action" value="deny" class="btn btn-lg btn-danger">No</button>
            </div>
        </form>
    }
</div>

@code {
    private string? applicationName;
    private string? errorMessage;
    private string? userCode;

    [SupplyParameterFromForm]
    private string? Action { get; set; }

    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    protected override async Task OnInitializedAsync()
    {
        // Retrieve the claims principal associated with the user code.
        var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        if (result is { Succeeded: true } && !string.IsNullOrEmpty(result.Principal.GetClaim(Claims.ClientId)))
        {
            // Retrieve the application details from the database using the client_id stored in the principal.
            var application = await ApplicationManager.FindByClientIdAsync(result.Principal.GetClaim(Claims.ClientId)!) ??
                throw new InvalidOperationException("Details concerning the calling client application cannot be found.");

            applicationName = await ApplicationManager.GetLocalizedDisplayNameAsync(application);
            userCode = result.Properties!.GetTokenValue(OpenIddictServerAspNetCoreConstants.Tokens.UserCode);
            return;
        }

        // If a user code was specified (e.g as part of the verification_uri_complete)
        // but is not valid, render a form asking the user to enter the user code manually.
        else if (!string.IsNullOrEmpty(result.Properties!.GetTokenValue(OpenIddictServerAspNetCoreConstants.Tokens.UserCode)))
        {
            errorMessage = "The specified user code is not valid. Please make sure you typed it correctly.";
            return;
        }
    }

    private async Task OnSubmitAsync()
    {
        switch (Action)
        {
            case "accept":
                // Retrieve the profile of the logged in user.
                var user = await UserManager.GetUserAsync(HttpContext.User) ??
                    throw new InvalidOperationException("The user details cannot be retrieved.");

                // Retrieve the claims principal associated with the user code.
                var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
                if (result is { Succeeded: true } && !string.IsNullOrEmpty(result.Principal.GetClaim(Claims.ClientId)))
                {
                    // Create the claims-based identity that will be used by OpenIddict to generate tokens.
                    var identity = new ClaimsIdentity(
                        authenticationType: TokenValidationParameters.DefaultAuthenticationType,
                        nameType: Claims.Name,
                        roleType: Claims.Role);

                    // Add the claims that will be persisted in the tokens.
                    identity.SetClaim(Claims.Subject, await UserManager.GetUserIdAsync(user))
                            .SetClaim(Claims.Email, await UserManager.GetEmailAsync(user))
                            .SetClaim(Claims.Name, await UserManager.GetUserNameAsync(user))
                            .SetClaim(Claims.PreferredUsername, await UserManager.GetUserNameAsync(user));

                    // Note: in this sample, the granted scopes match the requested scope
                    // but you may want to allow the user to uncheck specific scopes.
                    // For that, simply restrict the list of scopes before calling SetScopes.
                    identity.SetScopes(result.Principal.GetScopes());
                    identity.SetResources(await ScopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
                    identity.SetDestinations(static claim => [Destinations.AccessToken]);

                    await HttpContext.SignInAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties
                    {
                        // This property points to the address OpenIddict will automatically
                        // redirect the user to after validating the authorization demand.
                        RedirectUri = "/"
                    });
                    return;
                }

                else
                {
                    errorMessage = "The specified user code is not valid. Please make sure you typed it correctly.";
                    return;
                }

            case "deny":
                await HttpContext.ForbidAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, new AuthenticationProperties
                {
                    // This property points to the address OpenIddict will automatically
                    // redirect the user to after rejecting the authorization demand.
                    RedirectUri = "/"
                });
                return;

            default:
                errorMessage = "Unrecognized action.";
                return;
        }
    }
}

Hope it'll help.

kevinchalet avatar Dec 08 '25 17:12 kevinchalet

@Saccu19 do you still need help? 😉

kevinchalet avatar Dec 12 '25 13:12 kevinchalet