Sachit Murarka
Sachit Murarka

Reputation: 183

How to add Refresh tokens in OpenId

I am using UseOpenIdConnectAuthentication

I have added scope as offline_access but if use below snippet then context.ProtocolMessage.RefreshToken. is not being found. Can anyone please help here?

Snippet of code:

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ClientId = clientId,
        Authority = Authority,
        ResponseType = "code",
        Resource = graphUrl,
        Scope = "openid profile offline_access User.ReadBasic.All User.Read.All Directory.Read.All",
        Notifications = new OpenIdConnectAuthenticationNotifications
        {
            RedirectToIdentityProvider = (context) =>
            {

                string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;

                if (redirectUris.Contains(appBaseUrl.ToUpperInvariant()))
                {
                    context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
                    context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
                }
                else
                {
                    context.ProtocolMessage.RedirectUri = redirectUris.First() + "/";
                    context.ProtocolMessage.PostLogoutRedirectUri = redirectUris.First();
                }
                return Task.FromResult(0);
            },
            SecurityTokenValidated = async context =>
            {
                try
                {
                    ClaimsIdentity claimsIdentity = context.AuthenticationTicket.Identity;

                    if (claimsIdentity.IsAuthenticated)
                    {
                        string userObjectID = claimsIdentity.FindFirst(userObjIdentifier).Value;                                 
                        if (context.AuthenticationTicket.Properties.ExpiresUtc.HasValue)
                        {
                            context.Response.Cookies.Append("AuthTokenExpiryTime", context.AuthenticationTicket.Properties.ExpiresUtc.Value.ToString());
                        }
                        var accessToken = context.ProtocolMessage.AccessToken;
                        if (!string.IsNullOrEmpty(accessToken))
                        {
                            claimsIdentity.AddClaim(new System.Security.Claims.Claim("access_token", accessToken));
                        }

                        //var refreshToken = context.ProtocolMessage.GetParameter("refresh_token");
                        //if (!string.IsNullOrEmpty(refreshToken))
                        //{
                        //    claimsIdentity.AddClaim(new System.Security.Claims.Claim("refresh_token", refreshToken));
                        //}

                        var refreshToken = context.AuthenticationTicket.Properties.Dictionary["refresh_token"];
                        if (!string.IsNullOrEmpty(refreshToken))
                        {
                            // Add the refresh token as a claim
                            context.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim("refresh_token", refreshToken));
                        }


                        //other code
                    }
                }
                catch (Exception ex)
                {
                    Trace.TraceError("Correlation ID: {0}, Exception while getting authentication token in startup.auth.cs. Source: {1}, ExceptionVerbose: {2}",
                    Trace.CorrelationManager.ActivityId,
                    ex.Source,
                    ex.ToString());
                    throw ex;
                }


            }
        }
    });

Upvotes: 0

Views: 132

Answers (1)

Rukmini
Rukmini

Reputation: 16064

Note that: UseOpenIdConnectAuthentication is obsolete. You should switch to using the newer approach, which involves configuring authentication using AddOpenIdConnect and AddAuthentication. Refer this MsDoc

To get access, ID and refresh tokens without making use of client secret, check the below:

Create a Microsoft Entra ID application and configure redirect URL under Mobile and desktop applications as https://localhost:7135/signin-oidc and enable Allow public client flows as YES:

enter image description here

Make sure to grant offline_access API permission:

enter image description here

My Startup.cs file looks like below:

enter image description here

namespace OpenIdConnectSample;

public class Startup
{
    public Startup(IConfiguration config, IWebHostEnvironment env)
    {
        Configuration = config;
        Environment = env;
    }

    public IConfiguration Configuration { get; set; }

    public IWebHostEnvironment Environment { get; }

    private void CheckSameSite(HttpContext httpContext, CookieOptions options)
    {
        if (options.SameSite == SameSiteMode.None)
        {
            var userAgent = httpContext.Request.Headers["User-Agent"].ToString();

            if (DisallowsSameSiteNone(userAgent))
            {
                options.SameSite = SameSiteMode.Unspecified;
            }
        }
    }

    public static bool DisallowsSameSiteNone(string userAgent)
    {
        if (string.IsNullOrEmpty(userAgent))
        {
            return false;
        }

        if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12"))
        {
            return true;
        }

        if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
            userAgent.Contains("Version/") && userAgent.Contains("Safari"))
        {
            return true;
        }

        if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
        {
            return true;
        }

        return false;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
            options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
            options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        });

        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie()
        .AddOpenIdConnect(o =>
        {
            o.ClientId = "ClientID"; // Client ID

            // Removed ClientSecret as we are using PKCE
            // o.ClientSecret = "your-client-secret"; 

            o.Authority = "https://login.microsoftonline.com/TenantID/v2.0";

            o.ResponseType = OpenIdConnectResponseType.Code;
            o.SaveTokens = true;
            o.GetClaimsFromUserInfoEndpoint = true;
            o.AccessDeniedPath = "/access-denied-from-remote";
            o.ClaimsIssuer = "https://sts.windows.net/TenantID/";
            o.Scope.Add("offline_access");

            o.ClaimActions.Add(new IssuerFixupAction());

            // Enable PKCE (Proof Key for Code Exchange)
            o.UsePkce = true;

            o.Events = new OpenIdConnectEvents()
            {
                OnAuthenticationFailed = c =>
                {
                    c.HandleResponse();
                    c.Response.StatusCode = 500;
                    c.Response.ContentType = "text/plain";
                    if (Environment.IsDevelopment())
                    {
                        return c.Response.WriteAsync(c.Exception.ToString());
                    }
                    return c.Response.WriteAsync("An error occurred processing your authentication.");
                }
            };
        });
    }

    public void Configure(IApplicationBuilder app, IOptionsMonitor<OpenIdConnectOptions> optionsMonitor)
    {
        app.UseDeveloperExceptionPage();
        app.UseCookiePolicy();
        app.UseAuthentication();

        app.Run(async context =>
        {
            var response = context.Response;

            if (context.Request.Path.Equals("/signedout"))
            {
                await WriteHtmlAsync(response, async res =>
                {
                    await res.WriteAsync($"<h1>You have been signed out.</h1>");
                    await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
                });
                return;
            }

            if (context.Request.Path.Equals("/signout"))
            {
                await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                await WriteHtmlAsync(response, async res =>
                {
                    await res.WriteAsync($"<h1>Signed out {HtmlEncode(context.User.Identity.Name)}</h1>");
                    await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
                });
                return;
            }

            if (context.Request.Path.Equals("/signout-remote"))
            {
                await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties()
                {
                    RedirectUri = "/signedout"
                });
                return;
            }

            if (context.Request.Path.Equals("/access-denied-from-remote"))
            {
                await WriteHtmlAsync(response, async res =>
                {
                    await res.WriteAsync($"<h1>Access Denied error received from the remote authorization server</h1>");
                    await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
                });
                return;
            }

            if (context.Request.Path.Equals("/Account/AccessDenied"))
            {
                await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                await WriteHtmlAsync(response, async res =>
                {
                    await res.WriteAsync($"<h1>Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'</h1>");
                    await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>");
                    await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
                });
                return;
            }

            var userResult = await context.AuthenticateAsync();
            var user = userResult.Principal;
            var props = userResult.Properties;

            // Not authenticated
            if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
            {
                await context.ChallengeAsync();
                return;
            }

            if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true")))
            {
                await context.ForbidAsync();
                return;
            }

            if (context.Request.Path.Equals("/refresh"))
            {
                var refreshToken = props.GetTokenValue("refresh_token");

                if (string.IsNullOrEmpty(refreshToken))
                {
                    await WriteHtmlAsync(response, async res =>
                    {
                        await res.WriteAsync($"No refresh_token is available.<br>");
                        await res.WriteAsync("<a class=\"btn btn-link\" href=\"/signout\">Sign Out</a>");
                    });

                    return;
                }
            }

            if (context.Request.Path.Equals("/login-challenge"))
            {
                await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new OpenIdConnectChallengeProperties()
                {
                    Prompt = "login",
                    Scope = new string[] { "openid", "profile", "offline_access" }
                });

                return;
            }

            await WriteHtmlAsync(response, async res =>
            {
                await res.WriteAsync($"<h1>Hello Authenticated User {HtmlEncode(user.Identity.Name)}</h1>");
                await res.WriteAsync("<a class=\"btn btn-default\" href=\"/restricted\">Restricted</a>");
                await res.WriteAsync("<a class=\"btn btn-default\" href=\"/login-challenge\">Login challenge</a>");
                await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>");
                await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout-remote\">Sign Out Remote</a>");

                await res.WriteAsync("<h2>Claims:</h2>");
                await WriteTableHeader(res, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value }));

                await res.WriteAsync("<h2>Tokens:</h2>");
                await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value }));
            });
        });
    }

    private static async Task WriteHtmlAsync(HttpResponse response, Func<HttpResponse, Task> writeContent)
    {
        var bootstrap = "<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css\" integrity=\"sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu\" crossorigin=\"anonymous\">";

        response.ContentType = "text/html";
        await response.WriteAsync($"<html><head>{bootstrap}</head><body><div class=\"container\">");
        await writeContent(response);
        await response.WriteAsync("</div></body></html>");
    }

    private static async Task WriteTableHeader(HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data)
    {
        await response.WriteAsync("<table class=\"table table-condensed\">");
        await response.WriteAsync("<tr>");
        foreach (var column in columns)
        {
            await response.WriteAsync($"<th>{HtmlEncode(column)}</th>");
        }
        await response.WriteAsync("</tr>");
        foreach (var row in data)
        {
            await response.WriteAsync("<tr>");
            foreach (var column in row)
            {
                await response.WriteAsync($"<td>{HtmlEncode(column)}</td>");
            }
            await response.WriteAsync("</tr>");
        }
        await response.WriteAsync("</table>");
    }

    private static string HtmlEncode(string content) =>
        string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content);

    private class IssuerFixupAction : ClaimAction
    {
        public IssuerFixupAction() : base(ClaimTypes.NameIdentifier, string.Empty) { }

        public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
        {
            var oldClaims = identity.Claims.ToList();
            foreach (var claim in oldClaims)
            {
                identity.RemoveClaim(claim);
                identity.AddClaim(new Claim(claim.Type, claim.Value, claim.ValueType, issuer, claim.OriginalIssuer, claim.Subject));
            }
        }
    }
}

When I run the project I got sign-in screen as below:

enter image description here

After sign-in access, ID and refresh token got generated successfully:

enter image description here

You can also refresh the access token refer the below GitHub blog:

aspnetcore/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs at main · dotnet/aspnetcore · GitHub by josephdecock.

Upvotes: 1

Related Questions