0

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
    {
       
        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));
                        }

                      

                       


                        //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;
                }


            }
        }
    });
20
  • Could you include more details like what you tried and where you stuck by editing your question with code and error? Commented Jan 22 at 5:06
  • I have added offline_access in the App Registrations. Have added scope in the code to be offline_access Previously the response type was "token id_token" . I kept it as "code id_token" now Upon debugging, I do not see access token value coming up. Also not sure how to extract refresh token Commented Jan 22 at 7:12
  • Can you add the code by editing the question? Commented Jan 22 at 7:13
  • Added the snippet Commented Jan 22 at 7:17
  • 1
    Okay will check in my environment and update Commented Jan 22 at 7:18

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

10 Comments

IWebHostEnvironment is not available, Is it a .NET FW specific?
I have implemented in core. Do you want to implement the logic in framework?
Yes we have .NET 4.8 We can not use client secret . I tried incorporating how you shared in the previous conversation by making separate HTTP call to get refresh token but it did not work
In my above code I have not used client secret
Yes , but when I was trying to make it compatible , it was saying PKCE wont work with .NET 4.8
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.