How to set acr-values for Sustainsys external provider in identity server 3 - saml-2.0

I have Idsvr3 with local user accounts in SQL. In addition i have also configured external identity provider which support SAML2 using https://github.com/Sustainsys/Saml2 I followed the sample here
Now when user access the client application he gets redirected to login page which presents userid/password textboxes for local login and also a button to redirect to external provider.
I want to change this behavior. I want user directly goto external login based on some condition. I've read that I can pass the required login provider to the acr_values and IdSvr3 will directly go to external provider.
Here is how i registered external provider with IdSvr3 (Note some code is removed for brevity)
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
var identityServerOptions = new IdentityServerOptions
{
AuthenticationOptions = new AuthenticationOptions()
{
}
.Configure(ConfigureIdentityProviders),
};
idsrvApp.UseIdentityServer(identityServerOptions);
});
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
// SAML2
var options = new Saml2AuthenticationOptions(false)
{
SPOptions = new SPOptions
{
EntityId = new EntityId("https://localhost:44300/IdSrv3/Saml2"),
},
SignInAsAuthenticationType = signInAsType,
Caption = "SAML2p"
};
UseIdSrv3LogoutOnFederatedLogout(app, options);
options.SPOptions.ServiceCertificates.Add(new X509Certificate2(
AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "/App_Data/Sustainsys.Saml2.Tests.pfx"));
options.IdentityProviders.Add(new IdentityProvider(
new EntityId("https://stubidp.sustainsys.com/Metadata"),
options.SPOptions)
{
LoadMetadata = true
});
app.UseSaml2Authentication(options);
}
}
and here is client application startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(CK);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44300/identity",
Scope = "openid profile email",
ClientId = "XXXXXXXXXXXXXXX",
RedirectUri = "http://localhost:36102/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = (n) =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest)
{
if(SomeCondition == true)
{
n.ProtocolMessage.AcrValues = "idp:saml2";
}
}
return Task.FromResult(0);
}
}
});
}
}
However identity server throws error External login error: provider requested saml2 is not a configured external provider
What is the valid name for Sustainsys/Saml2 provider and where is it configured?

I think i found it. The idp is actually the value of AuthenticationType property.
During external provider setup in IdentityServer3, the Saml2AuthenticationOptions by default sets the AutheticationType to Saml2.
So in client application i have to use exact same value as acr-values, it is case-sensitive. I was using small s instead of capital S. When i changed to Saml2 it worked.
I can also override AutheticationType to any string i want, and that is good because now i can setup multiple external IdP that supports SAML2 protocol and differentiate them by their AutheticationType
Also i found this documentation helpful
https://media.readthedocs.org/pdf/saml2/latest/saml2.pdf
Take a look how okta is configured with IdentityServer3 in section 2.5.4 Step 3: Configure your identity server with the new identity provider
Also from IdentityServer documentation
AuthenticationType must be a unique value to identify the external
identity provider. This value will also be used for the idp claim in
the resulting tokens. Furthermore the same value can be used to
pre-select identity providers during authorization/authentication
requests using the acr_values parameter (see this for more
information). This value is also used to restrict the allowed identity
providers on the Client configuration.

Related

IdentityServer4 Windows Authentication Missing Callback implementation

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("~/");
}

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.

Custom endpoint for authorized clients on Identity Server 4

I want my Identity Server 4 server to offer an additional service (e.g., "MyAdditionalService") for SOME of the registered clients. That service will be consumed by them through a custom endpoint to be defined on the server.
I am thinking of defining an API for my that service (e.g., named "myAdditionalService") so that the access to such service can be granted to clients according to their configuration. However I am not sure how to restrict the access to the Endpoint (MVC - Action method) allowing only the clients (potentially on behalf of a user) that are allowed to consume the API.
I found out that I can do:
services.AddAuthorization(options =>
{
options.AddPolicy("MyAdditionalServicePolicy",
policy => policy.RequireClaim("scope",
"myAdditionalService"));
});
and use the attribute [Authorize("MyAdditionalServicePolicy")] to decorate the action method that is used to access such service. However, I don't know can the server be the API at the same time or even if it is possible.
How can I implement this? It is confusing that the token service plays the role of the API as well, since it protects access to an action method or endpoint.
Thanks.
UPDATE:
My web app is an IdentityServerWithAspNetIdentity which already use the Authentication mechanism of Asp.net core Identity. For the sake of the example, the additional service my web app if offering to some registered clients is the list of Twitter friends of a user (Modeled on a controller called Twitter, action called ImportFriends) the api is consequently called "TwitterFriends"
As per suggestion in response below, I modified my Configure() method to have app.UseJwtBearerAuthentication(). I already had app.UseIdentity() and app.UseIdentityServer() as shown below:
app.UseIdentity();
app.UseIdentityServer();
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AuthenticationScheme = "Bearer",
Authority = Configuration["BaseUrl"],
Audience = "TwitterFriends",
RequireHttpsMetadata = false //TODO: make true, it is false for development only
});
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
SignInScheme = "Identity.External", // this is the name of the cookie middleware registered by UseIdentity()
And on a dedicated controller:
[Authorize(ActiveAuthenticationSchemes = "Identity.Application,Bearer")]
//[Authorize(ActiveAuthenticationSchemes = "Identity.Application")]
//[Authorize(ActiveAuthenticationSchemes = "Bearer")]
[SecurityHeaders]
public class TwitterController : Controller
{...
but I am getting this in the log:
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware
[7]
Identity.Application was not authenticated. Failure message: Unprotect tic
ket failed
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed for user: (null).
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.A
uthorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
Executing ChallengeResult with authentication schemes (Identity.Applicatio
n, Bearer).
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware
[12]
AuthenticationScheme: Identity.Application was challenged.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action IdentityServerWithAspNetIdentity.Controllers.TwitterContro
ller.ImportFriends (IdentityServerWithAspNetIdentity) in 86.255ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 105.2844ms 401
I have tried different combinations of the attribute but it seems that Identity.Application and Bearer don't get along in this scenario: getting 401.
any help is appreciated.
Thanks..
See this example on how to host an API in the same web app as IdentityServer.
https://github.com/brockallen/IdentityServerAndApi
In essence you need to add the JWT token validation handler:
services.AddAuthentication()
.AddJwtBearer(jwt =>
{
jwt.Authority = "base_address_of_identityserver";
jwt.Audience = "name of api";
});
On the API itself you must select the JWT authentication scheme:
public class TestController : ControllerBase
{
[Route("test")]
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult Get()
{
var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToArray();
return Ok(new { message = "Hello API", claims });
}
}
If you want to enforce an additional authorization policy, you can either pass that into the [Authorize] attribute or call it imperatively.
To achieve this, first you have to write some policy. Policy will define the boundry of accessibility of that specific api.
So you will assign the some scope to registered clients. let's say scope name is "ApiOnlyForRegisteredClients".
So we will create the policy as below:
services.AddAuthorization(options =>
{
options.SetRegisteredClientsPolicy();
}
and
private static void RequireScope(this AuthorizationPolicyBuilder authorizationPolicyBuilder, string[] values)
{
authorizationPolicyBuilder.RequireClaim("scope", values);
}
private static void SetRegisteredClientsPolicy(this AuthorizationOptions options)
{
options.AddPolicy(
OpenIdPolicies.Clients.RegisteredClients,
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.RequireScope(new string[] { "ApiOnlyForRegisteredClients" });
});
}
Once it done, you are done with policy creation.
Make sure while creating the access token, you are put the same value "ApiOnlyForRegisteredClients" in scope claim.
Now we have to add one api and label it with [Authorize] attribute.
[Authorize(AuthenticationSchemes = "Bearer", Policy = OpenIdPolicies.Clients.RegisteredClients)]
public async Task<ActionResult<T>> Post(int userId, [FromBody] List<int> simRoleIds)
{
}
Now we have to add jwt authentication middleware.
.AddJwtBearer("Bearer", options =>
{
options.Authority = configuration["AuthorityAddresses"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["RequireHttpsMetadata"]);
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
TokenDecryptionKey = new X509SecurityKey()
ValidAudiences = apiResources.Select(x => x.ResourceName).ToList(),
ValidIssuers = new List<string> { authorityAddressWithHttps.Uri.OriginalString, authorityAddressWithBasePathHttps.Uri.OriginalString, configuration["AuthorityAddresses"] }
};
})

Azure B2C Persistent Cookie

I am using Azure B2C with one identity provider configured (LinkedIn). I have a Web API (b2c bearer auth) and a Web App MVC (b2c Open Id).
I'm trying to create a persistent login - meaning the user can login via LinkedIn once every 90 days from the given device+browser.
The closest I've gotten is when I added IsPersistent = true code in the web app to enable that:
Update: Updated code based on Azure B2C GA. To achieve where I was at with Preview, I still use a custom authorize attribute, but the code was updated:
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties()
{
IsPersistent = true
});
base.HandleUnauthorizedRequest(filterContext);
}
However, this is only valid for about 1 hour. Perhaps it is following the Access & ID policy? With no bounds on the refresh token - I am not sure why only 1 hour for "IsPersistent".
Token Session Config in Azure
So that leads to these questions:
Is a Persistent session (60-90 days) something I can achieve with Azure B2C (OpenId Connect)?
If so, any pointers on what I am missing? Do I need to do some custom cookie validation? Something with refresh tokens (I use them for the web api, but nothing custom in the web app).
Any thoughts or input would be great!
I have been able to achieve a persistent session with B2C after doing the following:
Custom Authorization Attribute
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.HttpContext.GetOwinContext()
.Authentication.Challenge(
new AuthenticationProperties() { IsPersistent = true }
);
base.HandleUnauthorizedRequest(filterContext);
}
Use Microsoft.Experimental.IdentityModel.Clients.ActiveDirectory instead of BootstrapContext (basically went with the pre-GA code sample (view change history) -> https://github.com/AzureADQuickStarts/B2C-WebApp-WebAPI-OpenIDConnect-DotNet). The ADAL library handles the getting a proper token transparent to my code.
Implemented custom TokenCache (based the EFADAL example here: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-multitenant-openidconnect/blob/master/TodoListWebApp/DAL/EFADALTokenCache.cs)
Changed Startup.Auth.cs:
return new OpenIdConnectAuthenticationOptions
{
MetadataAddress = String.Format(aadInstance, tenant, policy),
AuthenticationType = policy,
UseTokenLifetime = false,
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
},
Scope = "openid offline_access",
ResponseType = "code id_token",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
SaveSigninToken = true,
},
}

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