User sign-in

Authentication of the user must be performed by the Identity Provider (IdP) (in this case by Authway), where the user´s session or credentials will be checked. It is an application that wants to authenticate the user, that triggers the sign-in flow by using either the OpenId Connect protocol (OIDC) (preferred) or the SAML protocol (only available in Enterprise Edition and less preferred, but sometimes necessary). The application is called Client in OIDC and Service Provider (SP) in SAML. The authentication is done by a web browser, running on the user´s device, by redirecting the user to the IdP. How the users are authenticated is left up to the IdP to decide (even if the application can pass parameters affecting how that decision is done).

Considerations for different kind of applications

A server-based web application can choose to use OIDC or SAML.

Mobile applications running on platforms such as iOS and/or Android should use the system browser to re-direct the user for authentication. Embedded web views are not trusted, since there is nothing that prevents the app from snooping on the user password. User authentication must always be done in a trusted context that is separate from the app.

Single Page Applications (SPA) or other browser (JavaScript) applications should carefully choose how to handle authentication and we have a separate page with more details about this.

Choose OpenId Connect Flow

OIDC has flows designed for the different application types.

  • Authorization code flow: This is the most used and the preferred flow, primarly used by server-base applications and native/mobile apps. Authentication is triggered by re-directing the user to the IdP, but when the user is returned only a code is passed back to the application through the user browser, never any token(s). Instead, the tokens are fetched by using a back-channel where the code is exchanged for the token(s). This is the most secure flow since the tokens are never exposed to the browser and it also makes it easy to authenticate the application (client). If possible, it also good to use this flow with PKCE.
  • Implicit flow: Can be used by SPA or other JavaScript applications that don’t have a backend. Token(s) are send directly to the browser during re-direction of the user after a successful sign-in. Therefor this flow is less secure and as detailed in our article you should consider to use a Backend for Frontend (BFF).
  • Hybrid flow: Rarely used nowdays and it is a combination of implicit and code flow.

SAML Authentication

Authway supports SAML authentication in the Enterprise edition, but is must be actively turned on by IRM. Authway supports both Redirect and Post Bindings.

Subsections of User sign-in

Choose Authentication for a SPA

When building a SPA there are two alternatives (both uses OpenId Connect protocol) for how to authenticate the users. Traditionally the authentication have been triggered by the end users browser, which means that the tokens are returned to the end users browser and then the token(s) are typically stored on the client. The token are then used when making API calls to the backend.

SPA-triggered-authentication.png SPA-triggered-authentication.png

There are a couple of challenges with this and the biggest one is that there are no real secure way to protected the token in the end users browser (resulting in a larger attack vector). A smaller problem is that the client can’t use a secret against the Identity Provider (if a secret is required, it must be send to the browser and in that case it is not secret anymore).

During the last couple of years the protection and restrictions around cookies have improved a lot and a cookie is therefor a much safer way to handle an authenticated user today. This has evolved a pattern called Backend-for-frontend which makes it possible to use cookies in the end users browser instead of tokens. The backend-for-frontend is just a proxy to the backend API (for example implemented by using YARP, Ocelot or another solution) that identifies the user through the cookie and then exchange the cookie to the corresponding token that is stored on the sever side and finally passes the token to the backend API.

BFF-triggered-authentication.png BFF-triggered-authentication.png

The result of this is that the token is securely stored on the server. To use this pattern, you will trigger the sign-in from the server (as in a traditional web application) instead of doing it from the client, for example before the SPA application is started. After the sign-in is completed the user is redirected (back) to the SPA client.

This is not just a more secure solution, but it is also easier in that it removes all CORS problems and it is typically easier to use refresh tokens on the server side than on the client.

OpenId Connect Authentication with Authorisation Code Flow

We strongly recommend the use of standardized and validated client libraries from you application code. Security is very difficult to get correct and this article will just show a simplified minimal implementation of the authentication of a user with OpenId Connect (OIDC), more in the purpose to educate the basics than showing production ready code.

Initiating user authentication

The application will initiate a user authentication by re-directing the browser to the authorization endpoint of the Identity Provider (IdP, in this case Authway). The re-direct must be an OIDC authentication request message. Here is an example of the re-direct by the application:

HTTP/1.1 302 Found
Location: https://YOURINSTANCE.irmciam.se/connect/authorize?
          client_id=YOUR_CLIENT_ID
          &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback
          &response_type=code
          &scope=openid
          &state=YOUR_STATE

Explanation of the request parameters in the example:

  • client_id: The application (Client) identifier in Authway. This identifier is created when the application is registered in Authway.
  • redirect_uri: The callback URI where the application want to retrieve the authentication response. This URI is validated by Authway and it must be an exact match of a registered redirect URI.
  • response_type: Set to code to indicate that it is the authorisation code flow that should be used.
  • scope: Used to specify the requested authorisation in OAuth. openid indicates a request for OIDC authentication and a ID token. More scopes can be supplied to indicate what information should be included in the ID token.
  • state: Value set by the application to keep state between the request and the callback.

There are more parameters that can be used.

At the Idp the user will be prompted to sign-in, if that is not already the case. After the Idp has successfully authenticated the user it will call the application redirect_uri with an authorization code (or an error code if Idp fails).

HTTP/1.1 302 Found
Location: https://client.example.org/callback?
          code=A_CODE_ISSUED_BY_IDP
          &state=YOUR_STATE

The application must validate the state parameter and if successful continue to exchange the code for the ID token.

Exchange code for ID token

The retrieved code is meaningless to the application and must be exchanged for an ID token by making a back-channel call to the Idp. By making this in a back-channel the client can be validated, so that the Idp is only revealing tokens to trusted applications, and the ID token is never exposed to the browser (which is more secure).

The code can be exchanged at the token endpoint:

POST /connect/token HTTP/1.1
Host: YOURINSTANCE.irmciam.se
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3

grant_type=authorization_code
 &code=A_CODE_ISSUED_BY_IDP
 &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback

Client ID and secret can be passed in the Authorization header or as client_id and client_secret parameters in the request body. The form-encoded parameters should include a repeated redirect_uri and the retreved code from the response after the successful authentication of the user.

If the Idp handles the request successfully it will return a JSON object with the ID token, an access token and optionally a refresh token together with expiration information.

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "id_token": "A_JWT_TOKEN",
  "access_token": "AN_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
}

The ID token should be validated by the application before it is accepted.

Authorization Endpoint Parameters

Request parameters

Parameter Required Description
acr_values no Space seperated string with special requests from the client. See below.
client_id yes The unique id of the requesting client.
code_challange no, but required for client configured for PKCE.
code_challenge_method
login_hint no Hint of the username. Could be used if the client asks for username before redirecting.
max_age no Specifies how much time (in seconds) that is allowed to been passed since user singed in. See Force re-authentication of user.
nonce no A random string value used to mitigate replay attacks by associating the client session with the ID token.
prompt no “none”, “login”, “consent”, “select_account” or “create”. Indicates how the client wants that Authway handles the request.
redirect_uri yes The callback URL the client wants to re-direct to.
request no
request_uri no
response_mode
response_type yes
scope yes A space separated string of scopes that the client wants access to.
sso_token no A non-standardized parameters to enable some extra single-sign-on scenarios.
state no A random value that will be passed back to the client. Can be used to keep track of a session or to prevent unsolicited flows.
ui_locales no End-User’s preferred languages, represented as a space-separated list of language tag values, ordered by preference. For instance, the value “sv-SE en” represents a preference for Swedish as spoken in Sweden, then English (without a region designation).

acr_values parameters

The acr_values parameters are passed as “parameter:value” and if multiple parameters are passed they should be seperated with a space. For example:

tenant:priv idp:bankid
Parameter Description
idp The unique identifier of the sign-in method to use. See Control authentication method from the client.
tenant The unique identifier of an owner (tenant) that the user must belong to. See Only allow users from specific tenant
impersonate Trigger the flow to impersonate another user. See Impersonate a user.

Default Claims Supported

This is the default claims supported by Authway, but custom support can be added for more/other claims in a customer instance. The scope that the claims belong to is also the default, but could differ for a specific customer.

Scope Claim Description
openid sub A unique value identifying the user.
openid tid A unique value identifying the tenant the user belongs to.
profile family_name The surname of the user. For example “Doe”.
profile given_name The given name of the user. For example “Joe”.
profile name The full name of the user (given_name + ’ ’ + family_name). For example “Joe Doe”.
profile preferred_username The user name used during sign-in, most commonly the e-mail address.
profile picture A URL with a user picture taken from Gravatar or the initials if the user does not have a Gravatar picture.
email email The email address of the user. Could exists twice if the user has another e-mail registered on the person.
email email_verified True if the email address have been verified and it is always the same e-mail address used as user name that are confirmed (or not).
phone phone_number The phone number of the user.
phone phone_number_verified True if the phone number have been verified; otherwise False.
org orgid The unique identifier for the organisation the user belongs to. Will be the same as tid for users belonging to the mother organisation, but could be different for users belonging to a subsidiary.
org orgin The organisation number identifying the organisation, if the value exists.
org company_name The name of the organisation.
roles role The name of the group the user belongs to. Zero, one or more claims of this type could exists depending on the number of groups the user belongs to.
perms perm The unique identifier of a permission that the user have. Zero, one or more claims of this type could exists depending on the number of permissions the user got.

Protocol claims

Claim Description
amr The authentication used when the user signed in. The value would be pwd for password or external for an external identity provider (idp). More than one amr claim can exists. For example if multi-factor authentication is used there will be an amr claim with the mfa value.
idp The identity provider used when amr is external or local if the user used a password to sign-in.
auth_time The time the user authenticated. Could be used to evaluate if a re-authentication is required.

There are more protocol claims, but those are the most commonly used claims by developers themselves.

Special claims

Impersonated users

If a user is impersonated there will be an act claim with a serialized json structure with standard claims from the original user performing the impersonation. For example:

"act": {
   "oid":"d5542f98-8a6f-6d2a-cda0-39fc52ae2b58",
   "sub":"295A0000-E969-E6E6-3826-08DB0DD1E036",
   "tid":"a27446b6-795e-4ccc-1da6-39fc52ae2b37"
}

If impersonation is used there will also be a second amr claim with the value imp (for impersonate).

Linked accounts

If linked account are used the sub claim will have the same value for all linked accounts (that is the definition of linked accounts) which means that it is not unique over all tenants anymore. In that case a oid claim (object id) is also issued with a true unique identifier over all tenants.

For linked accounts the a may_login claim is also issued for org scope, which contains a serialized JSON array with information about other organisations that the user could switch to. For example:

"may_login": {
   [{
      "oid":"d5542f98-8a6f-6d2a-cda0-39fc52ae2b58", 
      "tid":"ffffffff-ffff-ffff-ffff-ffffffffffff",
      "companyname": "Privatpersoner"
   }]
}

OpenId Connect Authentication with .NET

To log in a user according to this guide, you must have configured an application (client) as a web application (server).

To log in a user, you use the OpenID Connect support available in .NET. This guide will show how it’s done in .NET 5, but for the most part, the process is similar regardless of the framework version.

Add the NuGet package for OpenID Connect:

<ItemGroup>   
  <PackageReference Include="IdentityModel" Version="5.0.1" />
  <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.3" />
</ItemGroup>

Here is also support for IdentityModel, which has several useful classes when working with OpenID Connect and claims.

Configure OpenID Connect and Cookies so that the application uses the IdP to identify users (in Startup.ConfigureServices):

services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "https://environment-company.irmciam.se/";

    options.ClientId = "Company.ApplicationId";
    options.ClientSecret = "supertopsecret";
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.MapInboundClaims = false;
});

In the project template, authentication is not enabled by default, a line must also be added in Startup.Configure (before UseAuthorization):

app.UseAuthentication();
app.UseAuthorization();

To trigger the login, there needs to be one or more protected pages. This can be configured in many different ways and is well-documented by Microsoft. For this example, we make a simple modification to the HomeController by adding the Authorize attribute, which forces the user to log in immediately.

[Authorize]
public class HomeController : Controller

Now you can test the application, and assuming everything is configured in the IdP, you should land directly on the IdP’s login page. After the login is completed, you will be redirected back to the application.

With the above configuration, User.Identity.Name may not display anything. This is because, for compatibility reasons, Microsoft reads the wrong claim, and the code must specify which claim to use instead. Make the following change to the options for the OpenID Connect configuration (which also sets the correct role claim):

...
options.SaveTokens = true;

options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
    NameClaimType = JwtClaimTypes.Name, //JwtClaimTypes.PreferredUserName, eller vad du önskar
    RoleClaimType = JwtClaimTypes.Role
};

JwtClaimTypes is part of the IdentityModel package that we added to the project file in the first step above.

To see which claims and other relevant information are available, you can add the following to a Razor view:

    <h2>Claims</h2>

    <dl>
        @foreach (var claim in User.Claims)
        {
            <dt>@claim.Type</dt>
            <dd>@claim.Value</dd>
        }
    </dl>

    <h2>Properties</h2>

    <dl>
        @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
        {
            <dt>@prop.Key</dt>
            <dd>@prop.Value</dd>
        }
    </dl>

What you quickly notice on a page displaying the user’s claims is that there are only a few claims compared to those available in the IdP. To get more claims, additional configuration is needed. All this configuration is also done in the AddOpenIdConnect call from above.

To retrieve more claims, you need to specify that more claims should be fetched from the UserInfo endpoint:

options.GetClaimsFromUserInfoEndpoint = true;

In OpenID Connect, the client (the application) requests which scopes it wants. Each scope is associated with one or more claims (this configuration is in the IdP). Common standard scopes include:

  • openid (must be requested).
  • profile (contains profile information for the user, such as name, picture, web pages, etc.).
  • email (contains email and whether the email address is verified).
  • phone (contains phone number and whether the number is verified, but this is not used by the IdP today).
  • address (contains address information, but is not used by the IdP today).
  • org (contains organizational information for users belonging to an organization; this is a custom scope for the IdP).

Microsoft’s default configuration of OpenID Connect automatically adds openid and profile, but if you want to include the user’s email, you need to add that scope to the configuration:

//Begär de scopes (gruppering av claims) som önskas
//options.Scope.Add("openid"); //Default by Microsoft
//options.Scope.Add("profile"); //Default by Microsoft
options.Scope.Add("email");
options.Scope.Add("org"); //IRM Idp organisationsinformation

If you run the application with these settings, you will unfortunately still notice that several claims are missing. This is because Microsoft has a concept called ClaimActions, which means that only the claims for which there is a ClaimAction will be transferred to the ClaimsIdentity during login. Therefore, we need to add more actions:

options.ClaimActions.MapUniqueJsonKey(JwtClaimTypes.Picture, JwtClaimTypes.Picture);
options.ClaimActions.MapUniqueJsonKey(JwtClaimTypes.EmailVerified, JwtClaimTypes.EmailVerified);
options.ClaimActions.MapUniqueJsonKey(Constants.JwtClaimTypes.TenantId, Constants.JwtClaimTypes.TenantId); //IRM IdP tenant id
options.ClaimActions.MapUniqueJsonKey(Constants.JwtClaimTypes.OrganisationId, Constants.JwtClaimTypes.OrganisationId); // IRM IdP organization id (same as before if the person belongs to the parent company, but may be different)
options.ClaimActions.MapUniqueJsonKey(Constants.JwtClaimTypes.CompanyName, Constants.JwtClaimTypes.CompanyName);

Several of these claims are unique to the IdP, and for this, we have created our own Constants class in the project (also declared in IRM.dll):

public static class Constants
{
    public static class JwtClaimTypes
    {
        public static string TenantId = "tid";
        public static string OrganisationId = "orgin";
        public static string CompanyName = "companyname";
        public static string Permission = "perm";
    }
}

With this configuration, you will see many of the claims that the IdP can provide about a user. In IRM.AspNetCpre.Mvc, there is a convenient extension method to add scopes and typical claim actions so that the application can access the claims included in each scope:

options.AddScope(StandardScopes.OpenId); //Default added by Microsoft, but here, a mapping is also done for OwnerId/TenantId
options.AddScope(StandardScopes.Profile); //Default added by Microsoft, but here, mappings are also done for, for example, picture.
options.AddScope(StandardScopes.Email);
options.AddScope(CustomScopes.Roles);
options.AddScope(CustomScopes.Permission);
options.AddScope(CustomScopes.Organisation);

Since we have specified SaveTokens = true above, we can retrieve the access token to make calls to an API:

var accessToken = await HttpContext.GetTokenAsync("access_token");

Note that if you retrieve many scopes and map many claims, it can result in a large number of claims, leading to a large cookie. In general, it’s good to keep the cookie size down, so this is something to consider. In AddCookie, there is an option to configure a SessionStore, allowing you to choose to store the content on the server instead, for example, in a distributed cache.

In IRM.AspNetCore.Mvc, there is a SessionStore that uses MemoryCache and/or DistributedCache if configured. Just call AddCacheSessionStore after AddCookie:

.AddCookie("Cookies", options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.Cookie.IsEssential = true;
}).AddCacheSessionStore()

Protect user information from the IdP

There are several challenges when requesting many scopes when a user (or system) is logging in:

  • The ticket can easily become a bit large, or especially the cookie can become large. It might sound strange, but it’s important to consider that when the cookie is saved, the user’s claims are stored, and often id_token, access_token, and possibly a refresh_token are also stored. This means that each claim can potentially exist two to three times in the cookie, and the size can quickly grow.
  • The ticket can contain sensitive information, such as personal information obtained when requesting profile, email, and similar scopes. Where will the ticket be stored? If it is to be sent to a user’s device to be included in API calls, there are not many great ways to protect the ticket and thus the sensitive information.

What can be done to reduce the size and increase security?

Avoid, if possible, sending any token to the client

Since there are no really good ways to securely protect a token on the client, the safest method is not to send any token to the client at all. Today, the recommended approach is to use cookies from the client because cookies can be securely managed (set them to HttpOnly and Strict) in an effective manner.

The challenge with cookies arises when making calls to an API with a different address than the website because the cookie is only valid on the website itself. If the client needs to make requests to an API at a different address, there are two alternatives:

  1. Send the token to the client, but preferably use a reference token. However, try to avoid this option if possible.
  2. Transform your web solution into an API gateway for the underlying API. Ensure that all API calls are routed back to the website, acting as an API gateway. For calls that need to proceed to an underlying API, forward them accordingly. When forwarding the request, append the access token associated with the logged-in user. It is not necessary to write many lines of code for this if you use a tool like YARP or Ocelot (you can also read more here).

Use a reference token instead of a JWT for your access token

An access_token can be of two types: 1) a JWT or 2) a reference token. The type an application receives is configured in the Identity Provider (IdP). A JWT contains all claims and some additional information, while a reference token is just a reference to the actual ticket. However, the real ticket must be fetched by the API from the IdP. The advantage of a reference token is that it is very small, and even if someone obtains the ticket, it contains no information by itself. The drawback of reference tokens is that the API must be able to handle this type of token, and typically, one would want the API to handle both JWT and reference tokens. This is well-documented, and there is some existing code that already supports it (you can start reading after the first bullet list, which is roughly in the middle of the blog post).

Request fewer scopes

Never request more scopes than absolutely necessary is a good general rule, but there is support in the Identity Provider (IdP) to complement with additional scopes from the server/API afterward. Besides the advantage of reducing the ticket size and containing less sensitive information, it also means that each application doesn’t need to know in advance which scopes each API it calls requires. The most crucial aspect is perhaps that it allows an API to evolve over time without requiring changes to calling applications.

Therefore, we recommend only fetching openid, the API scopes for the APIs to be called, and possibly the offline scope if a refresh token is needed. If you choose to proceed this way, both the web application and the API will need logic to fetch additional scopes. Here is example code for retrieving claims for additional scopes:

public Task<List<Claim>> GetAuthorizationAsync(ClaimsPrincipal user, List<string> scopes)
{
    if (!user.Identity.IsAuthenticated)
        return Task.FromResult(new List<Claim>());

    const string formEncoded = "application/x-www-form-urlencoded";

    var userId = user.FindFirst("sub")?.Value;
    if (string.IsNullOrEmpty(userId)) //Ingen autentierad användare, åtminstone inte någon som vi kan hämta behörigheter, profile eller något annat för
        return Task.FromResult(new List<Claim>());

    var cacheKey = CreateCacheKey(userId, scopes);

    return _cache.GetOrCreateAsync(cacheKey, async entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_options.CacheLength);

        var clientId = _options.ApiName;
        var clientSecret = _options.ApiSecret;

        var arguments = new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("client_id", clientId),
            new KeyValuePair<string, string>("client_secret", clientSecret),
            new KeyValuePair<string, string>("user_id", userId),
            new KeyValuePair<string, string>("scopes", string.Join(" ", scopes))
        };

        using (var content = new FormUrlEncodedContent(arguments))
        using (var client = new HttpClient())
        {
            content.Headers.ContentType = new MediaTypeHeaderValue(formEncoded) { CharSet = "UTF-8" };
            client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", formEncoded);

            using (var response = await client.PostAsync(_baseUri + "api/client/authorization", content).ConfigureAwait(false))
            {
                response.EnsureSuccessStatusCode();

                using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
                using (var reader = new StreamReader(stream))
                {
                    stream.Position = 0;

                    var serverClaims = Serializer.Deserialize<List<Claim>>(new JsonTextReader(reader));
                    return serverClaims;
                }
            }

        }
    });

}

ApiName and ApiSecret are obtained when an API is configured in the Identity Provider (IdP). If it’s a web application, ClientId and ClientSecret can be sent instead. In the code above, _baseUri needs to be set to the URL of the Auth server.

This function can then be called from, for example, a ClaimsTransformation class.

This logic is also necessary when using the Client Credentials flow (External systems) to make calls to your APIs. Fundamentally, a ticket for a Client cannot contain any personal claims since it represents a system/application. However, the IdP has specific support called External systems, allowing an API not to distinguish between a user or a calling system, simplifying many aspects.

In IRM.AspNetCore.Authorization.Client, there is ready-to-use support that is easy to configure, achieving exactly this.

In the call to AddCookie, there is the possibility to configure a SessionStore that allows saving the content of the cookie on the server instead, making the cookie itself just a reference. Here’s an example using IMemoryCache, but it can certainly be switched to IDistributedCache or some other type of storage.

public class MemoryCacheTicketStore : ITicketStore
{
    private readonly IMemoryCache _cache;
    private const string CacheKeyPrefix = "MemoryCacheTicketStore-";

    public MemoryCacheTicketStore(IMemoryCache cache)
    {
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
    }

    public async Task<string> StoreAsync(AuthenticationTicket ticket)
    {
        string key = await _backingTicketStore.StoreAsync(ticket);
        AddToCache(key, ticket);

        return key;
    }

    public async Task RenewAsync(string key, AuthenticationTicket ticket)
    {
        AddToCache(key, ticket);
    }

    private void AddToCache(string key, AuthenticationTicket ticket)
    {
        var options = new MemoryCacheEntryOptions
        {
            Priority = CacheItemPriority.NeverRemove
        };
        var expiresUtc = ticket.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.AddMinutes(20);
        options.SetAbsoluteExpiration(expiresUtc);

        _cache.Set(key, ticket, options);
    }

    public async Task<AuthenticationTicket> RetrieveAsync(string key)
    {
        _cache.TryGetValue(key, out AuthenticationTicket ticket);
        return ticket;
    }

    public async Task RemoveAsync(string key)
    {
        _cache.Remove(key);
    }
}

In IRM.AspNetCore.Authentication, there is ready-made support for using the cache as a session store:

AddCookie("Cookies", options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.Cookie.IsEssential = true;
    }).AddCacheSessionStore();

Here, a primarily distributed cache will be used if configured, but the support falls back to MemoryCache if it is not available.

Only allow users from a specific tenant to sign in

In some situations, you might want to control that only users that belong to a specific tenant (organisation) are allowed to sign in. It can e.g. be that a module/system is only used by internal users or only private individuals, etc.

There are two possible solutions that can be used:

  1. Connect the application to a module in configuration. The modules are enabled for the organisations (tenants) that should be able to use them. If a user belonging to an organisation that does not have access to the module tries to access it, the sign-in will be prevented and the user informed that they don’t have access. This can be used for applications that uses OpenId Connect or SAML protocol.
  2. In code it is possible by sending “tenant:GUID” (or “tenant:shortname”) in OpenId Connect parameter acr_values.

C# example

.AddOpenIdConnect(options =>
{
    //Other configuration not shown
    options.Events.OnRedirectToIdentityProvider = ctx =>
    {
        ctx.ProtocolMessage.AcrValues = "tenant:" + tenantId;

        return Task.CompletedTask;
    };
});

Control authentication method from the Client (application)

In some situations an application want to control how the user should be authenticated, for example require the user to use BankId to sign-in. This can be done in two ways:

  1. By configuration. Configure the client to only allow one authentication method and this will be the only choice possible for the user. This option is easiest if it should always be the same for all users. This option is supported for both OpenId Connect and SAML applications.
  2. Pass authentication method as argument in OpenId Connect acr_Values parameter. This will allow the application to pass different values for different users/situations.
  3. It is possible to pass arguments from a SAML application that requires or disables local sign-in. This is done by passing Password Protected Transport (urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport) as requested authn context together with Exact as comparison to require local sign-in, or Better as comparison to disable local sign-in.

Pass authentication method in acr_values

To pass authentication method you should use “idp:schemename” where schemename is the authentication method, for example local (to only use local passwords) or bankid to use Swedish BankId (for instance where this is configured). Valid values is per tenant.

BankId

As stated above the application can pass bankid as a value for idp. This will result in a prompt where the user will have to choose to use BankId on same device or on another device. It is also possible for the application to pass bankid-samedevice to shortcut the prompt and take the user immediately to sign-in on same device; or bankid-otherdevice to use another device to sign-in.

C# example

.AddOpenIdConnect(options =>
{
    //Other configuration not shown
    options.Events.OnRedirectToIdentityProvider = ctx =>
    {
        ctx.ProtocolMessage.AcrValues = "idp:bankid";

        return Task.CompletedTask;
    };
});

Limitaitons of controlling authentication methods

It is important that the application only force specific authentication methods that all (valid) users can use.

Force re-authentication of a user

In some situations, you might want to force a user to log in again. One situation could be if sensitive data is to be changed.

We recommend that this is done by sending the OpenId Connect parameter prompt with the value login. The exact same behavior can be achieved by setting ForceAuthentication to true in a SAML application.

Another option is to use max_age with the value 0 (or another number to specify how long the time is accepted). Using this option will notify the user that the application requires a new login.

Example of using prompt

C# example

[HttpGet]
public IActionResult Reauthenticate()
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = Url.ActionLink(nameof(Index))
    };
    properties.SetParameter<string>(OpenIdConnectParameterNames.Prompt, "login");
    return Challenge(properties);
}

By setting the parameter the OpenIdConnectHandler will automatically use the value.

Example of using max_age

C# example

[HttpGet]
public IActionResult Reauthenticate()
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = Url.ActionLink(nameof(Index))
    };
    properties.SetParameter(OpenIdConnectParameterNames.MaxAge, TimeSpan.zero);
    return Challenge(properties);
}

By setting the parameter the OpenIdConnectHandler will automatically use the value.

Verify that the user has re-authenticated

It is possible to verify that the user have re-authenticated again by checking the auth_time claim.

C# example

var foundAuthTime = int.TryParse(User.FindFirst("auth_time")?.Value, out int authTime);

if (foundAuthTime && DateTimeOffset.UtcNow.ToUnixTimestamp() - authTime < MaxAgeAllowed)
{
}
else
{
}

Impersonate (run as) a user

This is only supported for OpenId Connect protocol.

Authway has support for impersonation (or run as) of another user for users with special permissions. A typical scenario is for a support organization to be able to run as the user to be able to see exactly the same things as that user. This is a very powerful functionality that should be used with caution and thoughtfulness. At minimum the application need to implement a way to show that the user right now is not using the application as her/himself but as another user and how the actions taken with someone else’s identity must be logged in an appropriate way. Authway always logs information about who is impersonating someone else, so there is some traceability there and this is also shown in audit logs.

Pre-requisites for impersonation

An application must be configured to allow impersonation to be able to use the functionality.

  1. There must be users that are granted the “Run as” or “Run as within my organisation” permission. These powerful permissions is not enabled for all tenants, so the first step is to enable it for the tenant that hosts support personal. Open “Security” module and look for “Run as” and/or “Run as within my organisation” functionalities. Open the functionality and add tenant permission. When this step is done, the functionality is available when setting permissions for a group (but only in selected tenant(s)).

  2. Open the application (that has implemented support for impersonation) and turn on support for “Allow impersonate”.

Implement support for displaying that a user is running as someone else (impersonate)

The application can detect that the user is in “Run as” mode by checking if there are a amr claim with the value “imp” (for impersonate). In that case there will also be a act claim, which contains the claims that represent the original user (serialized as JSON):

{
   "sub": "5d9b6b01-c038-4b8d-bd98-ac9d7a3d0d4d",
   "name": "Bagarn Olsson",
   "tid": "e23dfa1b-bf65-4a04-ac7c-44d9b3edc1bc",
   "act": {
      "sub": "243a7798-11cc-4856-866b-834d1c4c8dff",
      "tid": "da9140ca-9759-45c7-ad3a-4bc7dafca0d1"
   }
}

It is possible for the application to control which claims should be in the act claim by sending the parameter claims in the call to the IdP and there list the claim types that are desired with a space. If no claims parameter is passed with, act will contain sub (Subject or unique identity of the user) and tid (tenant id or unique identity of the tenant, which may be the same or different from the user’s tid depending on who you run as). Remember to limit the number of claims as it affects the size of the login ticket.

Trigger an impersonate sign-in

There are two main options for triggering the impersonate sign-in flow:

  1. Send the unique identity (sub value) of the user to impersonate
  2. Let the user search and choose user to impersonate in Authway

Both alternatives will use a custom acr_values parameter called impersonate.

Option 1: Send the unique identity (sub value) of the user to impersonate

This option requires that the application, triggering the impersonate sign-in flow, already knows the unique identity of the user that should be impersonated. Because of this, Option 2 might be easier to implement in most situations.

For this option the application sends impersonate:{sub} as acr_values.

C# Example

Add an Impersonate operation in a Controller:

[HttpGet]
public IActionResult Impersonate(string sub)
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = "/"
    };
    properties.SetParameter(OpenIdConnectParameterNames.AcrValues, $"impersonate:{sub}");
    return Challenge(properties);
}

Handle RedirectToIdentityProvider event for the OpenIdConnectOptions so that the acr_values are send to Authway:

.AddOpenIdConnect(options =>
{         
    //Other configuration not displayed
    options.ClaimActions.MapUniqueJsonKey(JwtClaimTypes.Actor, JwtClaimTypes.Actor);

    options.Events.OnRedirectToIdentityProvider = ctx =>
    {
        if (ctx.Properties.Parameters.ContainsKey(OpenIdConnectParameterNames.AcrValues))
        {
            ctx.ProtocolMessage.AcrValues = ctx.Properties.GetParameter<string>(OpenIdConnectParameterNames.AcrValues);
        }

        return Task.CompletedTask;
    };
});

Option 2: Let the user search and choose user to impersonate in Authway

This is easiest to implement, since the application does not need to know anything about users. The application will rather just trigger the impersonate sign-in flow with the value select_account for the impersonate parameter.

C# Example

Add an Impersonate operation in a Controller:

[HttpGet]
public IActionResult Impersonate()
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = "/"
    };
    properties.SetParameter(OpenIdConnectParameterNames.AcrValues, "impersonate:select_account");
    return Challenge(properties);
}

In the same way as above, the event RedirectToIdentityProvider for the OpenIdConnectOptions should be handled so that the acr_values are send to Authway:

.AddOpenIdConnect(options =>
{         
    //Other configuration not displayed
    options.ClaimActions.MapUniqueJsonKey(JwtClaimTypes.Actor, JwtClaimTypes.Actor);

    options.Events.OnRedirectToIdentityProvider = ctx =>
    {
        if (ctx.Properties.Parameters.ContainsKey(OpenIdConnectParameterNames.AcrValues))
        {
            ctx.ProtocolMessage.AcrValues = ctx.Properties.GetParameter<string>(OpenIdConnectParameterNames.AcrValues);
            //Request name claim (default is only sub and tid returned)
            if (ctx.ProtocolMessage.AcrValues.StartsWith("impersonate:"))
                ctx.ProtocolMessage.Parameters.Add("claims", "sub tid name"); 
        }

        return Task.CompletedTask;
    };
});

In this example the claims parameter is also send to request an extra claim (name) to be supplied in the act claim.

Revert an impersonate sign-in back to original user

When the user is done and want to return to her/himself again, the application should send impersonate: without value and then the user will be returned with tokens representing the original user (without act and amr claim with value “imp”).

It is also possible to directly run as yet another user by sending impersonate: select_account again (or one of the other options above). When doing so, Authway will first sign-out the current impersonation and then the original user will have to search for a new user to run as.

Force single-sign-on from a Client (Application)

This requires OpenId Connect protocol and it isn’t supported in Starter Edition.

There are scenarios where you might want to force a single-sign-on from a client, that has already identified the user. This requires a trust relation between the Identity Provider and the Client. The trust is established by using a shared secret (that must be protected and handled with care of course) and by configuring the client to be allowed to send a single-sign-on token.

Scenario: Internal/on-prem system that identifies users through a local AD sign-in

If an on-prem system/application has identified the users without using the Identity Provider, but wants to open an application that uses the Identity Provider to identify users, the user will not get a single-sign-on experience (since the user isn’t signed in at the Identity Provider). This can be solved if the application can create a SSO token that can be verified the Identity Provider.

Scenario: Application that uses refresh tokens for a long time

Applications that keep the user signed in by using refresh tokens to refresh the sign-in can have a signed-in user that isn’t signed in, in the Identity Provider anymore. For example if the Identity Provider is configured to keep the user signed in for 10 days and the application is confiugred for sliding refresh tokens and using them to keep the user signed in for 20 days. When this application then redirects the user to another application, the user will not ge a singe-sign-on experience.

Force a single-sign-on between applications

Authway has a non-standard support where applications can pass a SSO token that is validated by the Identity Server to force a user to be signed in.

  1. Create a SSO token (see below)
  2. Redirect the user to the application according to section 4 (“Initiating Login from a Third Party”) in OpenId Connect core specification. The iss parameter is required and should be the address to the Identiy Provider. Pass the serialized SSO token in a non-standard parameter sso_token. The sso_token must be passed on to the Identity Provider when the target application redirects the user to sign-in.

Step 2 can be completed in a non-standard way by making an agreement between the applications, as long as the sso_token is passed to the Identity Provider.

Create a SSO token

The SSO token must fullfil these requirements:

  1. A valid JWT token.
  2. Use the HS256 algorithm to sign the token.
  3. Hash the shared client secret with SHA256 and use a base64 encoded hashed value as key when signing the token.
  4. The issuer must be the client id that creates the SSO token.
  5. Include an audience claim with the Identity Provider as audience.
  6. Include an issued at (iat) claim with the time when the client created the SSO token.
  7. Include a sub claim with the unique identifier of the user that should be signed in.

Sample token (without signature):

{
  "alg": "HS256",
  "typ": "JWT"
}.
{
  "sub": "FEFC9E8B-062B-DF44-FDF0-39FC52AE2B58",
  "iat": 1674050819,
  "iss": "TestTrustedApp",
  "aud": "https://tenant.irmciam.se"
}

C# example of creating a SSO token

var subClaim = new Claim("sub", "FEFC9E8B-062B-DF44-FDF0-39FC52AE2B58"); //Get claim value from signed-in user
var clientId = "TestTrustedApp";
var clientSecret = "W7k1i3EvpYSApLj6CW7pYGkYsFTGdwJ96m0uIh64";
var authority = "https://tenant.irmciam.se";
var key = Encoding.ASCII.GetBytes(clientSecret.Sha256()); //= Client secret

var credentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature);
            
var tokenDescriptor = new SecurityTokenDescriptor {
    Subject = new ClaimsIdentity(new Claim[] { subClaim }),
    Issuer = clientId, 
    IssuedAt = DateTime.UtcNow,
    Audience = authority, 
    SigningCredentials = credentials
};
            
var tokenHandler = new JwtSecurityTokenHandler();

var token = (JwtSecurityToken)tokenHandler.CreateToken(tokenDescriptor);

var serializedToken = tokenHandler.WriteToken(token);
public static string Sha256(this string input)
{
    if (input.IsMissing()) return string.Empty;

    using (var sha = SHA256.Create())
    {
        var bytes = Encoding.UTF8.GetBytes(input);
        var hash = sha.ComputeHash(bytes);

        return Convert.ToBase64String(hash);
    }
}

Python example of creating a SSO token

This sample is still not verified.

This example uses of PyJWT (https://pyjwt.readthedocs.io/en/latest/usage.html).

import jwt
import base64
from hashlib import sha256

sub = "FEFC9E8B-062B-DF44-FDF0-39FC52AE2B58"
clientId = "TestTrustedApp"
clientSecret = "W7k1i3EvpYSApLj6CW7pYGkYsFTGdwJ96m0uIh64"
authority = "https://tenant.irmciam.se"
key = sha256(clientSecret.encode("utf-8"))
base64_bytes = base64.b64encode(key)
key = base64_bytes.decode("ascii")

payload = {"sub": sub, "iss": clientId, "aud": authority, "iat": 1674050819}

serializedToken = jwt.encode(payload, key, algorithm="HS256")

Limitations to be aware of

If a client passes another user, than the currently signed-in user (at the Identity Provider), the current user will be signed out at the Identity Provider, but this will not be a complete single-sign-out effecting applications until those are refreshed (because that isn’t possible to do during a sign-in).

The client must know the unique identifier of a user to be able to create the SSO token.

Switch linked user

In scenarios where linked users are used there can be situation where an application want to trigger a switch to another of the linked users then the one currently using the application.

If the application requests the org scope during sign-in a may_login claim is included in the claims for users that have other linked users. The may_login claim is a JSON object with an array of users that are linked to the current user.

Example of a may_login claim

Here is an example with typical claims in a token for a user that has two linked child users to a parent user.

"sub": "dd41355c-95d9-4bf1-9c21-523b5b40f9f4",
"oid": "e4b8a6ff-cdb1-45f8-b255-8df7a09a9596",
"tid": "567c9683-4603-4279-9e53-ed77b060fe72",
"companyname": "Organisation A",

"may_login": [
    {
        "oid": "dd41355c-95d9-4bf1-9c21-523b5b40f9f4", 
        "tid": "ffffffff-ffff-ffff-ffff-ffffffffffff", 
        "companyname": "Privatpersoner"
    },
    {
        "oid": "4a7b708d-b7d3-4931-b3be-1d86e72214a5", 
        "tid": "1b30a7a4-b271-493a-a315-d35e976f11cf", 
        "companyname": "Organisation B"
    }
]

Explanation of thoose claims:

  • The sub claim will be the same for all users. This is the definition of a linked user.
  • The oid claim will only exist for users that have a different unique identifier in Authway, than the sub claim. It will be emitted for child accounts, but not for the parent.
  • The tid claim identifies the current tenant id and the name of the organisation is in companyname claim.
  • The may_login contains two entries in the array and each entry has claims explained above.
  • The user in tenant “Privatpersoner” is the parent user. This can be concluded because the value of oid is the same as sub claim.
  • There are no information about the users eventual permissions to the application in the may_login claim. If that information is needed it can be requested through the admin APIs.

Trigger a switch to a specific user

To change the current user to one of the other two users that exists in the may_login claim can be done by passing either the tenant or the oid as parameter to the authorize endpoint.

The tenant id for the user is in the tid claim as explained above. So to switch to the user that belongs to “Organisation B” in the example above, the tenant id 1b30a7a4-b271-493a-a315-d35e976f11cf should be passed as explained in “Only allow users from a specific tenant to sign in”.

The other alternative is to pass the oid value as user_id parameter to the authorize endpoint. Here is an example where a typical re-direct to the autorize endpoint is extended with the user_id parameter with the value of oid claim from Organisation B in the example above:

https://YOURINSTANCE.irmciam.se/connect/authorize?
          client_id=YOUR_CLIENT_ID
          &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback
          &response_type=code
          &scope=openid
          &state=YOUR_STATE
          &user_id=4a7b708d-b7d3-4931-b3be-1d86e72214a5