I am attempting to use ws-federation with .net core to handle single sign on. I have been able to receive a security token from my federated service, but the claims retrieved from the security token are not getting added to the user that's getting authenticated against, and even though a cookie is being written to after the token has been validated, the user does not get an updated set if identities or claims, leaving the user in a state where HttpContext.User.Identity.IsAuthenticated is always set to false.
My ConfigureServies:
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
services.AddControllersWithViews();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}
).AddWsFederation(options =>
{
WsFederationConfiguration configuration = new WsFederationConfiguration();
configuration.Issuer = "http://localhost/STSSpike2/V1";
configuration.TokenEndpoint = "http://localhost/STSSpike/V1";
options.Configuration = configuration;
options.Wtrealm = "http://localhost/STSAwareApp/Test";
options.Wreply = "http://localhost/STSAwareApp/signin-wsfed";
options.SecurityTokenHandlers.Add(new FakeTokenValidator());
options.Events.OnAuthenticationFailed = context =>
{
Console.WriteLine(context.Exception.ToString());
return Task.CompletedTask;
};
options.Events.OnAccessDenied = context =>
{
Console.WriteLine(context);
return Task.CompletedTask;
};
options.Events.OnRemoteFailure = context =>
{
Console.WriteLine(context.Failure);
return Task.CompletedTask;
};
options.Events.OnSecurityTokenReceived = context =>
{
Console.WriteLine(context.ProtocolMessage);
return Task.CompletedTask;
};
options.Events.OnSecurityTokenValidated = context =>
{
Console.WriteLine(context);
return Task.CompletedTask;
};
}).AddCookie(options =>
{
options.Cookie.Name = "TestStsAuth1";
options.ExpireTimeSpan = new TimeSpan(0, 30, 0);
options.Cookie.IsEssential = true;
//options.Cookie.HttpOnly = true;
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = val =>
{
Console.WriteLine("validated");
return Task.CompletedTask;
},
OnRedirectToAccessDenied = denied =>
{
Console.WriteLine("validated");
return Task.CompletedTask;
},
OnRedirectToReturnUrl = redirect =>
{
Console.WriteLine("validated");
return Task.CompletedTask;
},
OnSignedIn = signedIn =>
{
Console.WriteLine("Signedin");
return Task.CompletedTask;
},
OnSigningIn = signingIn =>
{
Console.WriteLine("signing in");
return Task.CompletedTask;
}
};
});
services.AddLogging(
builder =>
{
builder.AddFilter("Microsoft", LogLevel.Trace)
.AddFilter("System", LogLevel.Trace)
.AddConsole();
});
}
In the code above, the Events.OnSecurityTokenValidated and CookieOptions.Events.OnSignedIn point to the Principal that are derived from the security token returned from ws-federation. However, the HttpContext.User is never updated with the new identity. So once the security token is validated, the client repeatedly calls back to the federated server to request a token, with the only log messages being:
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed.
The problem came from my Configure call in that I called UseAuthorization before I called UseAuthentication. This answer was the key to finding out what teh issue was.
Related
I have an issue where no matter which settings I adjust, a user who has signed in is forcibly logged out after 30 minutes. I would like to extend this timeframe to 6 hours.
I am using a system where login credentials are provided externally. The system receives a JWT and uses the information contained there to authenticate the user. This is the root cause of the issue; users that log in via the .NET Framework's built-in authentication system don't have this problem. I don't know why or how.
Here are the highlights of what I have tried:
configuring application cookies in Program.cs:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.ExpireTimeSpan = TimeSpan.FromHours(6);
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
});
Using persistence and authentication properties in the authentication process:
await _signInManager.SignInAsync(
user,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6)
});
I have ensured I have data protection enabled in Program.cs:
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
I have published it to a dedicated IIS server where I set the Idle Timeout to 6 hours.
I have set SlidingExpiration = true in all the places that have the option to do so.
I have tried all of the above together, separate, and in combination. User sessions are still restricted to 30 minutes. When I set the Application Cookies to expire after 1 second, the session expires after 1 second. But when I set them to 6 hours, it still expires at 30 minutes. I have no idea why.
What am I missing here? I have been struggling with this for days and have still found no solution. I've found similar questions on Stack Overflow and their solutions all include what I have tried here, to no avail.
I have double-checked and noticed that the 30 minute time-out does not affect users that authenticate via the .NET Framework's built-in login system (where they enter their username/password). This only seems to affect users that authenticate using the credentials delivered via JWT.
Here is the full Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("ConnectionString");
builder.Services.AddDbContext<DBContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddControllersWithViews();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(6);
});
//Add JWT bearer and denied paths
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Jwt:Key"))
};
});
//GDPR compliance
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
builder.Services.ConfigureNonBreakingSameSiteCookies();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = x =>
{
x.Response.Redirect("https://localhost:44329/Expired/Index/");
return Task.CompletedTask;
}
};
options.ExpireTimeSpan = TimeSpan.FromDays(14);
});
//define policy for different authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Administrator"));
options.AddPolicy("UsersOnly", policy => policy.RequireRole("User", "Editor", "Author"));
options.AddPolicy("RequireApprovedUser", policy => policy.Requirements.Add(new ApprovedUserRequirement(true)));
});
builder.Services.AddScoped<IAuthorizationHandler, ApprovedUserRequirementHandler>();
//Data Protection configuration
var keysFolder = Path.Combine(builder.Environment.ContentRootPath, "UserKeys");
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<DBContext>();
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(6);
options.Lockout.MaxFailedAccessAttempts = 5;
options.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultTokenProviders()
.AddDefaultUI()
.AddEntityFrameworkStores<DBContext>();
builder.Services.AddRazorPages();
//this will get the email settings in appsettings
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
//Use for sending email
builder.Services.AddTransient<IMailService, Project.Services.EmailSender>();
// Register the Google Analytics configuration
builder.Services.Configure<GoogleAnalyticsOptions>(options => builder.Configuration.GetSection("GoogleAnalytics").Bind(options));
// Register the TagHelperComponent for Google Analytics
builder.Services.AddTransient<ITagHelperComponent, GoogleAnalyticsTagHelper>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, UserClaimsPrincipalFactory<IdentityUser, IdentityRole>>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode >= 400)
{
context.Request.Path = "/Error/Index/" + context.Response.StatusCode;
await next();
}
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(builder => builder.AllowAnyOrigin()
.WithOrigins("https://localhost:44329", "https://localhost:44329")
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
Here is the code that authenticates incoming users:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}
Here are the settings I used in IIS. Note that I will have trouble adjusting settings unavailable on Plesk.
Welp, it took a lot of digging, but I found the solution.
I needed to ensure that users were saved to the IdentityUser tables using _userManager.CreateAsync(). The code for registering users I used would create a new IdentityUser every time without ensuring it went in the database. Without an entry in the IdentityUser tables, .NET Core can't use keys or sessions properly.
Here is the change to that code:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser? iUser;
//check to see if user is in IdentityUser
iUser = await _userManager.FindByEmailAsync(user.Email);
//if user is not in IdentityUser, create a new one:
if(iUser == null)
{
iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
//Very important! This allows the system to remember users
//Performing this operation also gives iUser the necessary keys
await _userManager.CreateAsync(iUser);
}
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}
I have an MVC .NET Core 3.1 application that uses Open ID connect for authentication and stores identity & tokens in cookies. The tokens need to be refreshed as they are used in some API requests our application does. I subscribe to ValidatePrincipal event and refresh the tokens there. The request goes OK, but cookies are not updated for some reason.
Startup.cs:
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.OnAppendCookie = (e) =>
{
e.CookieOptions.Domain = <some domain>
};
});
...
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.Cookie.Domain = <some domain>;
options.Cookie.IsEssential = true;
options.Cookie.Name = <some name>
options.EventsType = typeof(CookieAuthEvents);
})
CookieAuthEvents.cs (constructor, member declarations and logging are omitted):
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
if (context.Principal.Identity.IsAuthenticated)
{
var now = DateTime.UtcNow;
var tokenExpiration = context.Properties.GetTokenExpiration();
if (now > tokenExpiration)
{
await UpdateCookies(context);
}
}
}
private async Task UpdateCookies(CookieValidatePrincipalContext context)
{
var refreshToken = context.Properties.GetTokenValue(OpenIdConnectGrantTypes.RefreshToken);
if (String.IsNullOrEmpty(refreshToken))
{
return;
}
var response = await GetTokenClient().RequestRefreshTokenAsync(refreshToken);
if (!response.IsError)
{
WriteCookies(context, response);
}
else
{
context.RejectPrincipal();
}
}
private void WriteCookies(CookieValidatePrincipalContext context, TokenResponse response)
{
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = response.IdentityToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = response.AccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = response.RefreshToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.TokenType,
Value = response.TokenType
}
};
var expiresAt = DateTime.UtcNow.AddSeconds(response.ExpiresIn);
tokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
});
var newPrincipal = GetNewPrincipal(context);
context.ReplacePrincipal(newPrincipal);
context.Properties.StoreTokens(tokens);
context.ShouldRenew = true;
}
private static ClaimsPrincipal GetNewPrincipal(CookieValidatePrincipalContext context)
{
var claims = context.Principal.Claims.Select(c => new Claim(c.Type, c.Value)).ToList();
var authTimeClaim = claims.FirstOrDefault(claim => claim.Type.Same("auth_time"));
if (authTimeClaim != null)
{
claims.Remove(authTimeClaim);
}
claims.Add(new Claim("auth_time", DateTime.UtcNow.UnixTimestamp().ToString()));
return new ClaimsPrincipal(new ClaimsIdentity(claims, context.Principal.Identity.AuthenticationType));
}
The main problem is this works perfectly fine locally. All the calls are fine, cookies are refreshed correctly. But when the app is run from dev machine (we host it in Azure App Service) RequestRefreshTokenAsync call is successful, but the cookies are not updated therefore all the next calls are made with an old tokens leading to 400 invalid_grant error. So basically what I see in logs is:
RequestRefreshTokenAsync successful. The old refresh_token is used to get a new one
ValidatePrincipal successful. Here the cookies should be rewritten (including a new refresh_token)
Next request - RequestRefreshTokenAsync failed. The old refresh_token is used (even though it's invalid).
I tried playing with cookies configurations and placing semaphores and locks inside ValidatePrincipal method but none of it worked.
Does anyone have an idea what can cause that?
I'm working on my hobby project where I'have implemented social login via Google.
now I want to prevent this so that only certain user can sign in into the app, As I found that there is no way to restrict this on google OAuth side, So I have added a table to store the email and role.
if the email address is not found in that table I want to prevent a user from signing.
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(googleOption =>
{
googleOption.ClientId = Configuration["Authentication:Google:ClientID"]; ;
googleOption.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
googleOption.Events.OnRemoteFailure = (context) =>
{
context.HandleResponse();
return context.Response.WriteAsync("<script>window.close();</script>");
};
googleOption.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents
{
OnTicketReceived = async ctx =>
{
string emailAddress = ctx.Principal.
FindFirstValue("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
var db = ctx.HttpContext.RequestServices.GetRequiredService<DbContext>();
var roles = await db.EmailRoles.Where(c => c.Email == emailAddress).ToListAsync();
if (roles.Count > 1)
{
var claims = new List<Claim>();
foreach (var item in roles)
{
claims.Add(new Claim(ClaimTypes.Role, item.Role));
}
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
}
};
});
I think you are looking for OnCreatingTicket. this will allow you to test the users as their logging in. In this example only gmail.com emails would be allowed to login anyone else would be kicked out
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle("Google", options =>
{
options.ClientId = Configuration["Authentication:Google:ClientId"];
options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
options.Events = new OAuthEvents
{
OnCreatingTicket = context =>
{
string domain = context.User.Value<string>("domain");
if (domain != "gmail.com")
throw new GoogleAuthenticationException("You must sign in with a gmail.com email address");
return Task.CompletedTask;
}
};
});
I have learnt IdentityServer4 for a week and successfully implemented a simple authentication flow with ResourceOwnedPassword flow.
Now, I'm implementing Google authentication with IdentityServer4 by following this tutorial
This is what I am doing:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//...
const string connectionString = #"Data Source=.\SQLEXPRESS;database=IdentityServer4.Quickstart.EntityFramework-2.0.0;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddProfileService<IdentityServerProfileService>()
.AddResourceOwnerValidator<IdentityResourceOwnerPasswordValidator>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
{
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
// Add jwt validation.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = "https://localhost:44386";
options.ClaimsIssuer = "https://localhost:44386";
// name of the API resource
options.ApiName = "api1";
options.ApiSecret = "secret";
options.RequireHttpsMetadata = false;
options.SupportedTokens = SupportedTokens.Reference;
});
//...
}
** Google controller (Which is for handling returned token from Google **
public class GLoginController : Controller
{
#region Properties
private readonly IPersistedGrantStore _persistedGrantStore;
private readonly IUserFactory _userFactory;
private readonly IBaseTimeService _baseTimeService;
private readonly ITokenCreationService _tokenCreationService;
private readonly IReferenceTokenStore _referenceTokenStore;
private readonly IBaseEncryptionService _baseEncryptionService;
#endregion
#region Constructor
public GLoginController(IPersistedGrantStore persistedGrantStore,
IBaseTimeService basetimeService,
ITokenCreationService tokenCreationService,
IReferenceTokenStore referenceTokenStore,
IBaseEncryptionService baseEncryptionService,
IUserFactory userFactory)
{
_persistedGrantStore = persistedGrantStore;
_baseTimeService = basetimeService;
_userFactory = userFactory;
_tokenCreationService = tokenCreationService;
_referenceTokenStore = referenceTokenStore;
_baseEncryptionService = baseEncryptionService;
}
#endregion
#region Methods
[HttpGet("login")]
[AllowAnonymous]
public IActionResult Login()
{
var authenticationProperties = new AuthenticationProperties
{
RedirectUri = "/api/google/handle-external-login"
};
return Challenge(authenticationProperties, "Google");
}
[HttpGet("handle-external-login")]
//[Authorize("ExternalCookie")]
[AllowAnonymous]
public async Task<IActionResult> HandleExternalLogin()
{
//Here we can retrieve the claims
var authenticationResult = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
var principal = authenticationResult.Principal;
var emailAddress = principal.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrEmpty(emailAddress))
return NotFound(new ApiMessageViewModel("Email is not found"));
// Find user by using username.
var loadUserConditions = new LoadUserModel();
loadUserConditions.Usernames = new HashSet<string> { emailAddress };
loadUserConditions.Pagination = new PaginationValueObject(1, 1);
// Find users asynchronously.
var loadUsersResult = await _userFactory.FindUsersAsync(loadUserConditions);
var user = loadUsersResult.FirstOrDefault();
// User is not defined.
if (user == null)
{
user = new User(Guid.NewGuid(), emailAddress);
user.Email = emailAddress;
user.HashedPassword = _baseEncryptionService.Md5Hash("abcde12345-");
user.JoinedTime = _baseTimeService.DateTimeUtcToUnix(DateTime.UtcNow);
user.Kind = UserKinds.Google;
user.Status = UserStatuses.Active;
//await _userFactory.AddUserAsync(user);
}
else
{
// User is not google account.
if (user.Kind != UserKinds.Google)
return Forbid("User is not allowed to access system.");
}
var token = new Token(IdentityServerConstants.TokenTypes.IdentityToken);
var userCredential = new UserCredential(user);
token.Claims = userCredential.GetClaims();
token.AccessTokenType = AccessTokenType.Reference;
token.ClientId = "ro.client";
token.CreationTime = DateTime.UtcNow;
token.Audiences = new[] {"api1"};
token.Lifetime = 3600;
return Ok();
}
#endregion
}
Everything is fine, I can get claims returned from Google OAuth2, find users in database using Google email address and register them if they dont have any acounts.
My question is: How can I use Google OAuth2 claims that I receive in HandleExternalLogin method to generate a Reference Token, save it to PersistedGrants table and return to client.
This means when user access https://localhost:44386/api/google/login, after being redirected to Google consent screen, they can receive access_token, refresh_token that has been generated by IdentityServer4.
Thank you,
In IdentityServer the kind (jwt of reference) of the token is
configurable for each client (application), requested the token.
AccessTokenType.Reference is valid for TokenTypes.AccessToken not
TokenTypes.IdentityToken as in your snippet.
In general it would be simpler to follow the original quickstart and then extend the generic code up to your needs. What I can see now in the snippet above is just your specific stuff and not the default part, responsible for creating the IdSrv session and redirecting back to the client.
If you still like to create a token manually:
inject ITokenService into your controller.
fix the error I mentioned above: TokenTypes.AccessToken instead of TokenTypes.IdentityToken
call var tokenHandle = await TokenService.CreateAccessTokenAsync(token);
tokenHandle is a key in PersistedGrantStore
I get an error
Cannot redirect to the end session endpoint, the configuration may be
missing or invalid when signing out.
when I handle signing out
public async Task LogOut()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync("oidc");
}
Schemas
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignOutScheme = "oidc";
sharedOptions.DefaultChallengeScheme = "oidc";
})
.AddCookie(options =>
{
options.AccessDeniedPath = new PathString("/Access/Unauthorised");
options.Cookie.Name = "MyCookie";
})
.AddOpenIdConnect("oidc", options =>
{
options.ClientId = Configuration["oidc:ClientId"];
options.ClientSecret = Configuration["oidc:ClientSecret"]; // for code flow
options.SignedOutRedirectUri = Configuration["oidc:SignedOutRedirectUri"];
options.Authority = Configuration["oidc:Authority"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = new PathString("/oidc");
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("pfidpadapterid", Configuration["oidc:PingProtocolMessage"]);
return Task.FromResult(0);
}
};
});
It seems like your authorization server does not support Session Management and Dynamic Registration. When it's supported, the discovery response contains end_session_endpoint. This is not the same as SignedOutRedirectUri, which is used as a final redirection target when the user is logged out on authorization server.
OnRedirectToIdentityProviderForSignOut event provides an option to set the issuer address, which in this case is the logout URI:
options.Events = new OpenIdConnectEvents()
{
options.Events.OnRedirectToIdentityProviderForSignOut = context =>
{
context.ProtocolMessage.IssuerAddress =
GetAbsoluteUri(Configuration["oidc:EndSessionEndpoint"], Configuration["oidc:Authority"]);
return Task.CompletedTask;
};
}
The helper method is used to support both relative and absolute path in the configuration:
private string GetAbsoluteUri(string signoutUri, string authority)
{
var signOutUri = new Uri(signoutUri, UriKind.RelativeOrAbsolute);
var authorityUri = new Uri(authority, UriKind.Absolute);
var uri = signOutUri.IsAbsoluteUri ? signOutUri : new Uri(authorityUri, signOutUri);
return uri.AbsoluteUri;
}
This way the authorization server will get additional parameters in the query string, which it can use e.g. to redirect back to your application.
To fix this handle the OnRedirectToIdentityProviderForSignOut event and specify the Logout endpoint manually:
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("pfidpadapterid", Configuration["oidc:PingProtocolMessage"]);
return Task.FromResult(0);
},
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = context =>
{
var logoutUri = Configuration["oidc:SignedOutRedirectUri"];
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};