Use (on-premise) Gitlab server as OAuth provider - asp.net-core

I want to make a .NET Core 2.1 MVC app use my internal Gitlab server as OAuth authentication service provider.
Inside Gitlab Admin Area I've added an Application:
Application Id: xxx
Secret: xxx
Callback url: http://localhost:5000/Account/ExternalLoginCallback
Trusted: Y
Scopes: - api (Access the authenticated user's API)
- openid (Authenticate using OpenID Connect)
The Startup.ConfigureServices is similar to:
services.AddAuthentication().AddOAuth("GitLab", options =>
{
options.ClientId = "xxx";
options.ClientSecret = "xxx";
options.CallbackPath = new PathString("/Account/ExternalLoginCallback");
options.AuthorizationEndpoint = "https://myGitlabServer/oauth/authorize";
options.TokenEndpoint = "https://myGitlabServer/login/oauth/token";
options.UserInformationEndpoint = "https://myGitlabServer/api/v4/user";
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
On navigating to my apps' login page I can select GitLab as login provider and get redirected to the login page successfully. After singning in with the correct creds I would expect the controller to be called but the redirect fails.
Exception: OAuth token endpoint failure: Status: NotFound;Headers: Server: nginx
The corresponding request is:
GET http://localhost:5000/Account/ExternalLoginCallback?code=xxx&state=xxx HTTP/1.1
The AccountController signature looks like:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
...
}
Any ideas what I am missing or doing wrong?

There where several things to be corrected.
First localhost does not apply to an application deployed somewhere in your network, so I had to change the Callback url from
http://localhost:5000/Account/ExternalLoginCallback
to
Callback url: http://{IP}:5000/signin-gitlab
Note the renaming of /Account/ExternalLoginCallback to just /signin-gitlab.
Startup.ConfigureServices (relevant parts) now:
...
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOAuth("Gitlab", options =>
{
options.SignInScheme = IdentityConstants.ExternalScheme;
// App creds
options.ClientId = "xxx";
options.ClientSecret = "xxx";
options.CallbackPath = new PathString("/signin-gitlab");
options.AuthorizationEndpoint = "https://myGitlabServer/oauth/authorize";
options.TokenEndpoint = "https://myGitlabServer/oauth/token";
options.UserInformationEndpoint = "https://myGitlabServer/api/v4/user";
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
// Add claims
var userId = user.Value<string>("username");
if (!string.IsNullOrEmpty(userId))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, userId);
}
var name = user.Value<string>("name");
if (!string.IsNullOrEmpty(name))
{
context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, name, ClaimValueTypes.String, context.Options.ClaimsIssuer));
options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, name);
}
var email = user.Value<string>("email");
if (!string.IsNullOrEmpty(email))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.Email, context.Options.ClaimsIssuer));
options.ClaimActions.MapJsonKey(ClaimTypes.Email, email);
}
var avatar = user.Value<string>("avatar_url");
if (!string.IsNullOrEmpty(avatar))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Uri, avatar, ClaimValueTypes.String, context.Options.ClaimsIssuer));
options.ClaimActions.MapJsonKey(ClaimTypes.Uri, avatar);
}
}
};
})
.AddCookie();
...
Inside my AccountController I have 2 methods to handle external logins:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider.
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
var claimsPrincipal = await this._signInManager.CreateUserPrincipalAsync(user);
var identity = claimsPrincipal.Identity as ClaimsIdentity;
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
var updateResult = await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user doesn't have an account, then ask to create one
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
}
Finally the view part inside my login view:
<section>
<h4>Use another service to log in.</h4>
<hr />
#{
var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (loginProviders.Count == 0)
{
<div>
<p>
There are no external authentication services configured. See this article
for details on setting up this ASP.NET application to support logging in via external services.
</p>
</div>
}
else
{
<form asp-action="ExternalLogin" asp-route-returnurl="#Context.Request.Query["returnUrl"]" method="post" class="form-horizontal">
<div>
<p>
#foreach (var provider in loginProviders)
{
<button type="submit" class="btn btn-gitlab" name="provider" value="#provider.Name" title="Log in using your #provider.DisplayName account"><i class="fab fa-#provider.Name.ToLower()"></i> #provider.Name</button>
}
</p>
</div>
</form>
}
}
</section>

Related

Trying to Authenticate using CookieAuthenticationDefaults.AuthenticationScheme with OpenIddict through angular client application

Trying to Authenticate using CookieAuthenticationDefaults.AuthenticationScheme with OpenIddict through angular client application but cookie not authenticated. does anyone know whats the issue
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginRequestDTO model)
{
var user = await _context.Users.Where(x => x.Username == model.Username).FirstOrDefaultAsync();
if(user == null)
{
return Ok(GenericResponse<bool>.Failure("Invalid Username or Password!", Models.Enums.ApiStatusCode.RecordNotFound));
}
else
{
if(user.Password != model.Password)
{
return Ok(GenericResponse<bool>.Failure("Invalid Username or Password!", Models.Enums.ApiStatusCode.RecordNotFound));
}
else
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, model.Username)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity));
return Redirect(model.ReturnUrl);
}
}
}
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
//options.LoginPath = "/login";
options.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = (context) =>
{
context.HttpContext.Response.Redirect("http://localhost:4200/login" + context.RedirectUri.Split("Login")[1]);
return Task.CompletedTask;
}
};
});

HttpContext.User.Identity.IsAuthenticated is false after calling HttpContext.SignInAsync("schema-name", claimsPrincipal);

I am using multiple authentication schemes. Here is my Startup.cs code -
public void ConfigureServices(IServiceCollection services)
{
//code
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Default";
options.DefaultSignInScheme = "Default";
options.DefaultChallengeScheme = "O365OpenId";
})
.AddCookie("Default",options =>
{
options.LoginPath = "/";
options.LogoutPath = "/Logout";
options.Cookie.Name = "ip" + guid.ToString();
options.AccessDeniedPath = "/Auth/Denied";
})
.AddCookie("External", options =>
{
options.LoginPath = "/Auth/External";
options.LogoutPath = "/Logout";
options.Cookie.Name = "ip" + guid.ToString();
options.AccessDeniedPath = "/Auth/Denied";
})
.AddOAuth("O365OpenId", options =>
{
//options.Authority = "given";
options.ClientId = "given";
options.ClientSecret = "given";
options.CallbackPath = "/auth/callback";
options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
options.AuthorizationEndpoint += "?prompt=select_account";
options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
options.UserInformationEndpoint = "https://graph.microsoft.com/oidc/userinfo";
options.AccessDeniedPath = "/Auth/Denied";
options.Scope.Add("openid");
options.Scope.Add("email");
//options.Scope.Add("profile");
//options.Scope.Add("offline_access");
//options.Scope.Add("user.read");
//options.Scope.Add("calendars.read");
options.SaveTokens = false;
//options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
//options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
//options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
//options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
//options.ClaimActions.MapJsonKey(ClaimTypes., "picture");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
if (context.AccessToken is { })
{
context.Identity?.AddClaim(new Claim("access_token", context.AccessToken));
}
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(json.RootElement);
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy =>
{
policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireRole("Admin");
});
options.AddPolicy("Regular&Admin", policy =>
{
policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireRole("Regular");
});
options.AddPolicy("External", policy =>
{
policy.AuthenticationSchemes.Add("External");
policy.RequireAuthenticatedUser();
policy.RequireRole("External");
});
});
}
When I sign in user using the "Default" scheme, I find that the HttpContext.User.Identity.IsAuthenticated is true after HttpContext.SignInAsync("schema-name", claimsPrincipal); method call. Here is the code for signing in -
public async Task<IActionResult> Success(string returnUrl)
{
// code
var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
await HttpContext.SignInAsync("Default",claimsPrincipal);
// more code
}
isAuthenticated is true
But When I sign in user using "External" scheme, HttpContext.User.Identity.IsAuthenticated is false after HttpContext.SignInAsync("schema-name", claimsPrincipal); call. Here is the code for signing in -
public async Task<IActionResult> External([FromQuery] string ReturnUrl, LoginOTP loginOTP, string email)
{
//code
var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
await HttpContext.SignInAsync("External", claimsPrincipal);
//code
}
IsAuthenticated is false.
I cannot understand what I am doing wrong. Why is the IsAuthenticated returning false?
Please help. Thank you.
you have 3 authentication schemes
the first one is cookie-based and you named it: "Default",
the second one is also cookie-based and you named it: "External",
the third one is OAhut-based and you named it: "O365OpenId",
when referring to one of these schemes you should use the names that you have created.
now the issue is, that you're creating a ClaimeIdentity with one scheme but you signing the user with a different scheme.
for the external method it should be like this:
var claimIdentity = new ClaimsIdentity(claims, "External");
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
// code ...
await HttpContext.SignInAsync("External", claimsPrincipal);
now the claims principal will be associated with the "External" scheme and the user will be authenticated.

OpenIddict Authorization Code Flow & Refresh Tokens

I've been experimenting with the OpenIddict sample projects, more specifically Zirku to better understand Authorization Code Flow and Introspection.
Based on a fair bit of research I've been able to develop a Client MVC Web App, an Auth Server, and a separate Resource Server (API), all of which were influenced by the samples linked above. In testing I've been able to login and access an endpoint from my API that is prefixed with the [Authorize] attribute successfully, by passing the access token in the request header. After waiting for a minute any attempt to access the API again, will result in a 401 Unauthorized as expected since the access token has now expired based on the Auth Server configuration. The only way to call the endpoint successfully after this, is to complete a logout and login thus generating a new access token and a grace period of a minute before it expires.
I've therefore implemented Refresh Tokens, through adding the RefreshTokenFlow and required offline_access scope to the relevant projects as seen below. Whilst I have the ability to obtain the access and refresh tokens in my Client application I am unsure on how to handle the process of using the refresh token to obtain a new access token.
In essence, how do I use the refresh token to obtain a new access token, once the original is nearing its expiry, and how can use the new token throughout my client application until it needs refreshing, or until the user has singed out? Presumably I need to call the connect/token endpoint with a grant_type of refresh_token, but will this update the HttpContext in my client app with the new tokens?
Client MVC:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
})
.AddOpenIdConnect(options =>
{
options.ClientId = "ExampleClientId";
options.ClientSecret = "ExampleClientSecret";
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Authority = "https://localhost:5001/";
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
options.Scope.Add("example_api");
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
});
...
app.UseAuthentication();
app.UseAuthorization();
Auth Server:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddDefaultUI();
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
options.ClaimsIdentity.EmailClaimType = Claims.Email;
options.SignIn.RequireConfirmedAccount = false;
});
builder.Services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
options.UseQuartz();
})
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetIntrospectionEndpointUris("/connect/introspect");
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, Scopes.OfflineAccess);
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.SetAccessTokenLifetime(TimeSpan.FromMinutes(1))
.SetRefreshTokenLifetime(TimeSpan.FromDays(1));
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
// Register the worker responsible for seeding the database.
// Note: in a real world application, this step should be part of a setup script.
builder.Services.AddHostedService<Worker>();
builder.Services.AddAuthorization();
...
app.UseAuthentication();
app.UseAuthorization();
Woker.cs:
public class Worker : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public Worker(IServiceProvider serviceProvider)
=> _serviceProvider = serviceProvider;
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.EnsureCreatedAsync();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("SampleClientMVC") == null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "ExampleClientId",
ClientSecret = "ExampleClientSecret",
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC Client Application",
PostLogoutRedirectUris =
{
new Uri("https://localhost:7001/signout-callback-oidc")
},
RedirectUris =
{
new Uri("https://localhost:7001/signin-oidc")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "example_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
if (await manager.FindByClientIdAsync("sample_resource_server") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "example_resource_server",
ClientSecret = "ExampleResourceServerSecret",
Permissions =
{
Permissions.Endpoints.Introspection
}
});
}
var scopeManager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
if (await scopeManager.FindByNameAsync("example_api") is null)
{
await scopeManager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "example_api",
Resources =
{
"example_resource_server"
}
});
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Token Endpoint:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsAuthorizationCodeGrantType())
{
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null)
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (!await _signInManager.CanSignInAsync(user))
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
Resource Server - API:
builder.Services.AddOpenIddict()
.AddValidation(options =>
{
options.SetIssuer("https://localhost:7235/");
options.AddAudiences("example_resource_server");
options.UseIntrospection()
.SetClientId("example_resource_server")
.SetClientSecret("ExampleResourceServerSecret");
options.UseSystemNetHttp();
options.UseAspNetCore();
});
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();
...
app.UseAuthentication();
app.UseAuthorization();
if you have saved the token in cookie,I think you could try as below to check the remaining time of the token ,and you could try to get a new token with httpclient
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = async CookieValiCoText =>
{
var now = DateTimeOffset.UtcNow;
var expiresAt = CookieValiCoText.Properties.GetTokenValue("expires_in");
.......some logical codes
//to get the accesstoken with refresh token if the token expires soon
if ( about toexpires )
{
var refreshToken = CookieValiCoText.Properties.GetTokenValue("refresh_token");
var response = await new HttpClient().RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = "your exchange end point",
ClientId = "ExampleClientId",
ClientSecret = "ExampleClientSecret",
RefreshToken = refreshToken
});
.......

Identity server identity token is always null

I'm using identity server with .net core application.
Current scenario: Getting token from Facebook, authenticating the user using Facebook API. And then creating a user in our database.
Everything is working fine, except that the returned token always have null identity_token.
I've tried to add id_token token to the scopes but didn't work also.
Login API:
[HttpPost("SocialLogin")]
public async Task<IActionResult> SocialMediaLogin()
{
try
{
var protocol = _httpContextAccessor.HttpContext.Request.Scheme;
var host = _httpContextAccessor.HttpContext.Request.Host.Value;
var baseUrl = $"{protocol}://{host}/";
#region Validating Headers and Parameters
StringValues jwtToken = Request.Headers["token"];
if (!jwtToken.Any())
{
return BadRequest("Each user should have a token");
}
var secretKey = _configuration["SecretKey"].ToString();
var (status, paylod) = GeneralUtilities.CheckSocialMediaTokenHeaderAndValues(jwtToken, secretKey);
if (status != ResponseStatus.Success)
{
return BadRequest("User Id or token are wrong");
}
#endregion
var clientId = _configuration["Clients:Mobile:Id"].ToString();
var secret = _configuration["Clients:Mobile:Secret"].ToString();
var consumerKey = _configuration["ConsumerKey"].ToString();
var consumerKeySecret = _configuration["ConsumerKeySecret"].ToString();
var disco = await DiscoveryClient.GetAsync(baseUrl);
if (disco.IsError) throw new Exception(disco.Error);
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, secret);
var payload = new
{
provider = paylod.Provider,
external_token = paylod.Token,
email = paylod.Email,
profilePicture = paylod.ProfilePictureUrl,
twitter_secret = paylod.TwitterSecret,
consumerKeySecret,
consumerKey,
displayName = paylod.DisplayName
};
//If user exist, we should update the profile Url of the user
var user = await _userManager.FindByEmailAsync(payload.email);
if (user != null)
{
user.ProfilePictureUrl = payload.profilePicture;
await _userManager.UpdateAsync(user);
}
var result = await tokenClient.RequestCustomGrantAsync(grantType: "external", scope: "my-api offline_access", extra: payload);
if (result.IsError) return new JsonResult(result.Json);
if (!string.IsNullOrWhiteSpace(result.AccessToken))
{
return Ok(result);
}
return new JsonResult(null);
}
catch (Exception)
{
return BadRequest();
}
}
As for the Startup.cs here's what I'm doing for Identity:
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddOperationalStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqloptions => sqloptions.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqloptions => sqloptions.MigrationsAssembly(migrationsAssembly)))
.AddAspNetIdentity<AppUser>()
.AddDeveloperSigningCredential();
// Configure Identity
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
});
Where does the identity_token or access_token are built?

Use external claims form OpenId server

I'm trying to use Keycloak as an OpenId server for my ASP.Net core app.
It's almost ok. The user gets authorized (passes through [Authorize] attribute), but his claims are empty.
My configuration:
services.AddAuthentication(options => { options.DefaultScheme = "cookie"; })
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:8080/auth/realms/master/";
options.RequireHttpsMetadata = false;
options.ClientId = "test-client";
options.ClientSecret = "ee117d6d-25c9-4317-83a0-54c2f252aa89";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.RemoteSignOutPath = "/SignOut";
options.SignedOutRedirectUri = "Redirect-here";
options.ResponseType = "code";
});
A controller action which works fine (gets authorized):
[Authorize]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
A controller action which is unauthorized (as it requires a role):
[Authorize(Roles = "Administrators")]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
AccountController.ExternalLoginCallback:
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync(); // it has claims
if (info == null)
{
return RedirectToAction(nameof(Login));
}
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
}
info objct from the snippet above has the claims, especially roles claims:
But when I'm logged in and try to see the roles and clams, the are emtpy:
var user = await _signInManager.UserManager.GetUserAsync(HttpContext.User); // user is found
var roles = await _signInManager.UserManager.GetRolesAsync(user); // empty
var claims = await _signInManager.UserManager.GetClaimsAsync(user); //empty
So it looks like Keycloak is doing everything fine, but my app has problems consuming the claims.
What am I missing?
You need to configure the authentication provider so it knows which claim type to use for roles.
You can configure it as follows:
services.AddOpenIdConnect("oidc", options =>
{
...
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "user_roles"; // or "user_realm_roles"?
};
});