IdentityServer4 Windows Authentication Missing Callback implementation - identityserver4

The documentation to setup Windows Authentication is here: https://docs.identityserver.io/en/latest/topics/windows.html
But I have no idea how to configure the Callback() method referred to in the line RedirectUri = Url.Action("Callback"), or wethere or not I'm even supposed to use that.
I tried manually redirecting back to the https://<client:port>/auth-callback route of my angular app but I get the error:
Error: No state in response
at UserManager.processSigninResponse (oidc-client.js:8308)
Does someone have a suggested Callback method I can use with an SPA using code + pkce ? I've tried searching Google but there are no current example apps using Windows Authentication and the ones that do exist are old.

Take a look at the ExternalLoginCallback method. I've also pasted the version of the code as of 26 Oct 2020 below for future reference incase the repo goes away.
/// <summary>
/// Post processing of external authentication
/// </summary>
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = await AutoProvisionUserAsync(provider, providerUserId, claims);
}
// this allows us to collect any additonal claims or properties
// for the specific prtotocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
additionalLocalClaims.AddRange(claims);
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps);
ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
// we must issue the cookie maually, and can't use the SignInManager because
// it doesn't expose an API to issue additional claims from the login workflow
var principal = await _signInManager.CreateUserPrincipalAsync(user);
additionalLocalClaims.AddRange(principal.Claims);
var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id;
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, name));
// issue authentication cookie for user
var isuser = new IdentityServerUser(principal.GetSubjectId())
{
DisplayName = name,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
// validate return URL and redirect back to authorization endpoint or a local page
var returnUrl = result.Properties.Items["returnUrl"];
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
}

Related

Keycloak - WPF check permission

I have a WPF app (.net 462 & API .net5.0).
I have set the openid on the api and this work, the permission work, but on the WPF app i have to check the permission for app element (menu access, button access).
Getting the token work, but i don't know how to valid a scope when openid
Key clock config :
Realm => demo-test
Client => demo-test-client
Client Role => DemoRole
Authorization Scope => demo:read
Associated Permissions => Permission demo:read
Associated Policy for role => DemoRole - Positive
I have create two user "user-test-ok" & "user-test-ko", "user-test-ok" have the client role "DemoRole".
I have test to user the introspection for validate the user have the scope "demo:read", but this not work.
I don't want use keycloak API, i want use the openid system for possibly change keycloak by other OAuth2.0 system.
This is my code to try to check the authorization scope :
var requestUri = new Uri($"https://localhost:8443/auth/realms/{realm}/protocol/openid-connect/token/introspect");
using (var client = new HttpClient())
{
var req = new HttpRequestMessage(HttpMethod.Post, requestUri);
req.Headers.Add("cache-control", "no-cache");
req.Headers.Add("accept", "application/x-www-form-urlencoded");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
req.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "token_type_hint", "requesting_party_token" },
{ "token", tokenResult.AccessToken },
{ "client_id", clientId },
{ "client_secret", clientSecret },
{ "scope", "test:read" },
});
var response = client.SendAsync(req).Result;
if (!response.IsSuccessStatusCode)
{
throw new Exception();
}
var responseString = response.Content.ReadAsStringAsync().Result;
}
Did you have any idea how to do it?
If I read the token introspection endpoint documentation here, then it says nothing about passing the scope ({ "scope", "test:read" },) as a parameter. Instead you take what you get back from that request and then you first check if the active claim is true. That signals that the token is still active and valid..
Then you just examine the data that is returned. Or you just use the scope value inside the access or ID-token to give the user access to the features in the application.
Do, check what the request returns in a tool like Fiddler.

Login after signup in identity server4

I am trying to login user as soon as he/she registers.
below is the scenario
1)Registration page is not on identity server.
2)Post user details to Id server from UI for user creation.
3)On successful user creation login the user and redirect.
4)Trying to do it on native app.
I tried it with javascript app but redirection fails with 405 options call.
(tried to redirect to /connect/authorize)
on mobile app, don't want user to login again after signup for UX.
Has anyone implemented such behavior
tried following benfoster
Okay so finally i was able to get it working with authorization code flow
Whenever user signs up generate and store a otp against the newly created user.
send this otp in post response.
use this otp in acr_value e.g acr_values=otp:{{otpvalue}} un:{{username}}
client then redirects to /connect/authorize with the above acr_values
below is the identity server code which handles the otp flow
public class SignupFlowResponseGenerator : AuthorizeInteractionResponseGenerator
{
public readonly IHttpContextAccessor _httpContextAccessor;
public SignupFlowResponseGenerator(ISystemClock clock,
ILogger<AuthorizeInteractionResponseGenerator> logger,
IConsentService consent,
IProfileService profile,
IHttpContextAccessor httpContextAccessor)
: base(clock, logger, consent, profile)
{
_httpContextAccessor = httpContextAccessor;
}
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
var processOtpRequest = true;
var isAuthenticated = _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated;
// if user is already authenticated then no need to process otp request.
if (isAuthenticated)
{
processOtpRequest = false;
}
// here we only process only the request which have otp
var acrValues = request.GetAcrValues().ToList();
if (acrValues == null || acrValues.Count == 0)
{
processOtpRequest = false;
}
var otac = acrValues.FirstOrDefault(x => x.Contains("otp:"));
var un = acrValues.FirstOrDefault(x => x.Contains("un:"));
if (otac == null || un == null)
{
processOtpRequest = false;
}
if (processOtpRequest)
{
var otp = otac.Split(':')[1];
var username = un.Split(':')[1];
// your logic to get and check opt against the user
// if valid then
if (otp == { { otp from db for user} })
{
// mark the otp as expired so that it cannot be used again.
var claimPrincipal = {{build your principal}};
request.Subject = claimPrincipal ;
await _httpContextAccessor.HttpContext.SignInAsync({{your auth scheme}}, claimPrincipal , null);
return new InteractionResponse
{
IsLogin = false, // as login is false it will not redirect to login page but will give the authorization code
IsConsent = false
};
}
}
return await base.ProcessInteractionAsync(request, consent);
}
}
dont forget to add the following code in startup
services.AddIdentityServer().AddAuthorizeInteractionResponseGenerator<SignupFlowResponseGenerator>()
You can do that by using IdentityServerTools class that IdentityServer4 provide to help issuing a JWT token For a Client OR a User (in your case)
So after the user signs up, you already have all claims needed for generating the token for the user:
including but not limited to: userid, clientid , roles, claims, auth_time, aud, scope.
You most probably need refresh token if you use hybrid flow which is the most suitable one for mobile apps.
In the following example, I am assuming you are using ASP.NET Identity for Users. The IdentityServer4 Code is still applicable regardless what you are using for users management.
public Constructor( UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IClientStore clientStore,
IdentityServerTools identityServerTools,
IRefreshTokenService refreshTokenService)
{// minimized for clarity}
public async Task GenerateToken(ApplicationUser user
)
{
var principal = await _signInManager.CreateUserPrincipalAsync(user);
var claims = new List<Claim>(principal.Claims);
var client = await clientStore.FindClientByIdAsync("client_Id");
// here you should add all additional claims like clientid , aud , scope, auth_time coming from client info
// add client id
claims.Add(new Claim("client_id", client.ClientId));
// add authtime
claims.Add(new Claim("auth_time", $"{(Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds}"));
// add audiences
var audiences = client.AllowedScopes.Where(s => s != "offline_access" && s != "openid" && s != "profile");
foreach (var audValue in audiences)
{
claims.Add(new Claim("aud", audValue));
}
// add /resources to aud so the client can get user profile info.
var IdentityServiceSettings = _configuration.GetSection("IdentityService").Get<IdentityServiceConsumeSettings>();
claims.Add(new Claim("aud", $"{IdentityServiceUrl}/resources"));
//scopes for the the what cook user
foreach (var scopeValue in client.AllowedScopes)
{
claims.Add(new Claim("scope", scopeValue));
}
//claims.Add(new Claim("scope", ""));
claims.Add(new Claim("idp", "local"));
var accesstoken = identityServerTools.IssueJwtAsync(100, claims);
var t = new Token
{
ClientId = "client_id",
Claims = claims
};
var refereshToken = refreshTokenService.CreateRefreshTokenAsync(principal, t, client);
}
This is just a code snippet that needs some changes according to your case

Authorization Flow Access and Refresh Tokens

Using Authorization Code does the middleware that intercepts signin-oidc exchange the authorization code for the access tokens or do I have to do this programatically? If the middleware does it, then were can I find the access and refresh tokens?
Or do I have to implement my own redirect url and code and capture the returned code and exchange it with the access tokens using RequestAuthorizationCodeTokenAsync?
No you do not have to implement the part to obtain the tokens this is handled by the handler, But you need a callback to handle the signin, storing claims and creating a login. Here is a primitive example of how to Obtain the Access Tokens:
EDIT
I will use Google as an example because I have the code on hand but the IdentityServer OAuth should be the same, seeing as they Extend OAuthHandler
services.AddAuthentication(options =>
{
//Add your identity Server schema etc
})
.AddGoogle(options =>
{
options.SaveTokens = true;
options.ClientId = Configuration["Google:ClientId"];
options.ClientSecret = Configuration["Google:ClientSecret"];
})
And in your Authentication controller:
[HttpPost("ExternalLogin")]
[AllowAnonymous]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet("ExternalLoginCallback")]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
throw new Exception($"Error from external provider: {remoteError}");
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
//It throws here, since there are no tokens
throw new Exception("Error: could not find user tokens");
}
//Handle the rest of authentication
}
What Happens? You have a button pointing to your External Login Provider "Google" as the provider.
You're redirected to the Google login page, and you login.
Google server redirects you back to you're domain and /google-signin (by default hidden in the handle) With the Authorization Code
The Google handler then uses the authorization code along with your secret to obtain the tokens
If you specify to save Tokens, in the OAuth Options, Tokens from the response will be saved. Along with some basic claims obtained from the user info endpoint.
You're then redirected to the External Login callback:
_signInManager.GetExternalLoginInfoAsync();
Will obtain the saved tokens.
So to answer your question. The handler will take care of saving tokens (If you specify it to). And you can obtain them from the signInManger if needed.

Identity server 4: intercept 302 and replace it with 401

I've got an app which is hosting simultaneously Identity Server 4 and a client app (Vue) which uses a couple of rest services defined in an area for managing the site. The idea is that users associated with a specific role can access the client app and call the rest services for performing the actions.
Currently, my problem is that when the api return 302 when the user doesn't belong to the admin role. I'd like to change this to a 401, but I'm having some problems with it.
If this was a simple aspnet core app, then I'd simply pass a lambda to the OnRedirectToLogin property of the cookie handler that takes care of the request. Unfortunately, IS4 will only allow me to set a couple of basic settings of the cookie (expiration and sliding). The same docs say that I can override the cookie handler. So, I've tried doing the following:
services.AddIdentityServer()
... // other configurations
services.AddAuthentication(sharedOptions => {
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;//IdentityServerConstants.ExternalCookieAuthenticationScheme;
sharedOptions.DefaultChallengeScheme = IdentityServerConstants.SignoutScheme;
})
... //other external providers...
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, options => {
options.Events = new CookieAuthenticationEvents {
OnRedirectToLogin = ctx => {
if (ctx.Request.Path.StartsWithSegments("/Admin", StringComparison.OrdinalIgnoreCase)) {
ctx.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
}
return Task.CompletedTask;
};
});
I expected to seem my handler being called whenever a request is redirected to the login page, but it never happens. Can anyone help?
Thanks
EDIT: just to add that I'm also using aspnet identity for managing the user accounts...
Posting the answer here in case anyone is interested...
After some digging, I've found that using identity means that you can't customize the cookie handler by doing what I was doing. Fortunately, the ConfigureAuthenticationEvent that can be configured by the ConfigureApplicationCookie extension method already does the right thing: if it detects that the current request is an AJAX call, it will return 401; if not, it will return 302. And here was the problem: the request made from the vue client wasn't being considered an AJAX request because it wasn't setting the X-Request-With header to XMLHttpRequest.
So, all it was required was to configure axios to set the header in all the calls:
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
I wrote a middleware sometime ago for this exact purpose and never looked back so if you don't find better solution, perhaps the solution can help you as well:
public class RedirectHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RedirectHandlingMiddleware> _logger;
public RedirectHandlingMiddleware(RequestDelegate next, ILogger<RedirectHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
await HandleRedirect(context, ex);
await _next(context);
}
private Task HandleRedirect(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/Admin", StringComparison.OrdinalIgnoreCase) && context.Response.StatusCode == 302)
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
return Task.CompletedTask;
}
}
Just need to register in Startup.cs:
app.UseAuthentication();
app.UseMiddleware<RedirectHandlingMiddleware>();

Asp.net core token based claims authentication with OpenIdConnect and angularjs: Bearer was forbidden

I'm using Asp.net core rc2 with OpenIdConnectServer. I'm using angular 1.x with augular-oauth2. After a few days, my error has digressed to
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:54275/api/Account/Username
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: Successfully validated the token.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: HttpContext.User merged via AutomaticAuthentication from authenticationScheme: Bearer.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was successfully authenticated.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was forbidden.
My ConfigureServices consists of
services.AddAuthorization(options =>
{
options.AddPolicy("UsersOnly", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("role");
});
});
My configure has
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
RequireHttpsMetadata = false,
Audience = "http://localhost:54275/",
Authority = "http://localhost:54275/",
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = "client1",
//ValidAudiences = new List<string> { "", "empty", "null"}
}
});
});
app.UseOpenIdConnectServer(options =>
{
options.AuthenticationScheme = OpenIdConnectServerDefaults.AuthenticationScheme;
options.Provider = new SimpleAuthorizationServerProvider();
options.AccessTokenHandler = new JwtSecurityTokenHandler();
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
options.TokenEndpointPath = new PathString("/oauth2/token");
options.LogoutEndpointPath = new PathString("/oauth2/logout");
options.RevocationEndpointPath = new PathString("/oauth2/revoke");
options.UseJwtTokens();
//options.AccessTokenLifetime = TimeSpan.FromHours(1);
});
My authorize attribute is defined on the Controller as
[Authorize(Policy = "UsersOnly", ActiveAuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme), Route("api/Account")]
I store the token as a cookie and attach it to requests using an http interceptor in angular.
I generate the token with
public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
{
// validate user credentials (demo mode)
// should be stored securely (salted, hashed, iterated)
using (var con = new SqlConnection(ConnectionManager.GetDefaultConnectionString()))
{
if (!Hashing.ValidatePassword(context.Password, await con.ExecuteScalarAsync<string>("SELECT PassHash FROM dbo.Users WHERE Username = #UserName", new { context.UserName })))
{
context.Reject(
error: "bad_userpass",
description: "UserName/Password combination was invalid."
);
return;
}
// create identity
var id = new ClaimsIdentity(context.Options.AuthenticationScheme);
id.AddClaim(new Claim("sub", context.UserName));
id.AddClaim(new Claim("role", "user"));
// create metadata to pass on to refresh token provider
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{"as:client_id", context.ClientId}
});
var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), props,
context.Options.AuthenticationScheme);
ticket.SetAudiences("client1");
//ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.Email, OpenIdConnectConstants.Scopes.Profile, "api-resource-controller");
context.Validate(ticket);
}
}
I've spent the last three days on this problem and I realize that at this point I'm probably missing something obvious due to lack of sleep. Any help would be appreciated.
The error you're seeing is likely caused by 2 factors:
You're not attaching an explicit destination to your custom role claim so it will never be serialized in the access token. You can find more information about this security feature on this other SO post.
policy.RequireClaim("role"); might not work OTB, as IdentityModel uses an internal mapping that converts well-known JWT claims to their ClaimTypes equivalent: here, role will be likely replaced by http://schemas.microsoft.com/ws/2008/06/identity/claims/role (ClaimTypes.Role). I'd recommend using policy.RequireRole("user") instead.
It's also worth noting that manually storing the client_id is not necessary as it's already done for you by the OpenID Connect server middleware.
You can retrieve it using ticket.GetPresenters(), that returns the list of authorized presenters (here, the client identifier). Note that it also automatically ensures a refresh token issued to a client A can't be used by a client B, so you don't have to do this check in your own code.

Resources