Reputation: 183
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
Reputation: 16064
Note that:
UseOpenIdConnectAuthentication
is obsolete. You should switch to using the newer approach, which involves configuring authentication usingAddOpenIdConnect
andAddAuthentication
. 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:
Make sure to grant offline_access API permission:
My Startup.cs
file looks like below:
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:
After sign-in access, ID and refresh token got generated successfully:
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