Subsections of Developer guide

Create a User

An administrator can create users in Authway admin UI and we always recommend to evaluate if auto-provisioning is possible since that remove a lot of manual (and double) administration. There are also valid reasons to use our admin APIs to create users.

A user in Authway is actually two objects, a person object and a user object. A person can exists without a user, but a user always requires a person. Both objects exists in a tenant and a person can be assigned to a subsidary if needed. There are scenarios where only a person should be created, but focus will be on users in this document.

Since the person/user is bound to a specific tenant all API calls to them is required to include tenant information as explained in the Introduction to admin APIs.

API:s to create a person and a user

Since a user requires a person, the normal way of creating a user is through the POST Person API. In the post it is possible to pass parameters so that both the person and the user is created in a single call. It is not possible to set password or other credentials for a user, but instead a user should be invited (or auto-linking can be used).

Please don’t pass the Id (it is not required) if not absolutely necessary as explained in the “Introduction to admin APIs”.

Invite a user

When creating a user through the POST Person API, there are a flag that can be set if you want Authway to send an invitation to the user. When setting the flag to true, it is also possible to pass additional parameters that should be added to the invitation link. Additional parameters:

  • clientId=[a client id]: The value of a registered application (client). When this parameter is passed the pages will use custom styles if that is registered for the client. The user will also be returned to URL registered on the client, when the account creation is completed.
  • link=true: Triggers the flow where a invited user will have to sign-in or create a personal account that the organisation account will be linked to. Read more about linking of users.

It is also possible to get an invitation link through the API if you want to share the link through other mechanisms or if more control over the e-mail creation is needed.

An invitation link is valid for 24 hours by default, but this can be configured in your instance.

Events during creation of person and user

There will be multiple events during the process of creating a user.

During the API calls to POST Person:

When the user uses the invitation link and completes the registration:

If a clientId parameter is included in the invitation link, the events will have metadata about the client (ClientId and ClientName); otherwise not.

Assign groups to a user

Normally it is also required to assign groups to the user so that the user will have permissions to do anything in the applications. Groups are assigned through the Group API. When adding groups it is possible to pass group id:s or group names, but it is not allowed to send a mix of both.

Subsections of Create a User

Link Users

Read about linked users so that you understand the concept and its limitations before continuing on this more developer focused documentation.

Create linked users

By invitation

Add query string parameter link=true to the invitation link will trigger a flow where the user must sign-in with (or create) a personal account which will do the linking automatically.

By API calls

To create users that should be linked you do the same as for a regular user. After the user is created, it is possible to link the user to a parent by POST users/{newly created user id}/link/{parent user id}.

Events during linking

A UserClaimAdded event is raised when a user is linked to a parent user. The event is raised to the child user (the same user that the link API was used for), which means that the AggregateId will have the value of the child user. The ClaimType will be “sub” and the ClaimValue will have the unique identifier of the parent user.

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

User sign-out

Depending on your situation, sign-out can be a more complicated matter than you first think. Below we describe three scenarios and in some cases also choices that can be made in each scenario.

Single-sign-out

Authway supports single-sign-out, which pretty much handles sign-out the same way as single-sign-on works. When an application triggers a sign-out, Authway will check which applications the user is singed in too, and try to sign the user out from each of the applications. Authway supports single-sign-out for both OpenId Connect (OIDC) and SAML protocols. There are two different ways to notify server-side client applications that the user has signed out and the most commonly supported way is to use a front-channel which works for both OIDC and SAML, but for OIDC we recommend back-channel if possible.

Front-channel sign-out for server-side applications

The front-channel sign-out notification is done via the browser and this the only method supported for SAML sign-out. To use front-channel sign-out, the application should register a single URL that should be invoked by Authway when a user sign-out. A call to this URL will typically be handled in a way that the application deletes the cookie that keeps track of the fact that the user is signed-in. The URL is called from a hidden iframe on the signed out page in Authway.

Please note that Front-channel sign-out is broken when browsers block third party cookies. This is because in most scenarios the application cookie won’t be passed and not deleted in the iframe, since it will be a third party cookie.

There are other drawbacks with front channel. It is restricted to a signed in user’s browser to perform the request for sign-out, which makes it impossible to handle scenarios where a user or an administrator wants to force a sign-out from everywhere (including from other devices). This why we recommend back-channel sign-out if possible.

OIDC Front-Channel Logout 1.0 specification

Back-channel sign-out for server-side applications

The back-channel sign-out notification is done through a server-to-server call, where Authway POST a logout (JWT) token to the registered back-channel URL. This is generally not supported out of the box in OIDC libraries so you’ll have to implement it yourself. It is important that the posted token is correctly validated.

OIDC Back-Channel Logout 1.0 specification

Sign-out for browser-based JavaScript applications

For browser-based applications it is not necessary to make any configuration in Authway, but the application must perform monitoring on the check_session_iframe, which is implemented by libraries that are compliant with the OIDC specification, for example oidc-client JavaScript library.

Only sign-out from the Application (but not from Authway or other applications)

There are valid scenarios where the user only want to sign-out from the current application and not a full single-sign-out. If this is a desired scenario the application shall not invoke the sign-out functionality, through OIDC or SAML, in Authway, but rather just remove the user session in the application (aka delete the sign-in cookie).

One problem with this solution is that when the user chooses to sign-in again it will happen automatically since the session is still alive in Authway. This can be confusing for users as it feels like they were not signed out. To counteract that experience, you can force the user to sign in again (se below), but then single-sign-on is instead lost for the application.

The best solution can sometimes be to let users choose if they should sign-out from the application only or if they want a full single-sign-out from all applications where there account is currently used.

Force new sign-in

Authway has support to force a user to make a new sign-in. This can be configured for the application or it can be triggered by passing parameters from the application. To configure the application to always require a new sign-in the “Users SSO time” should be set to a low value, like 5 seconds. The other alternative is to control from the application if a new sign-in should be required or not. When using the OIDC protocol this is done by passing parameter max-age=0 and for SAML this is done by setting ForceAuthentication.

Sign-out from application and Authway (but no other applications)

Another alternative is to sign-out from the application and Authway, but not registering front-channel or back-channel for any applications. When signing out in Authway all refresh tokens are invalidated which in many scenarios still will result in a situation where users are forced to sign-in again, for example all applications that uses access- and/or refresh-tokens. Applications that only uses Authway for authentication can often handle this without affecting the users.

Sign-out without showing Authway signed out page

Some applications prefer to show their own signed out page after a sign-out instead of Authway signed out page. This is challenging to fulfil while still supporting OIDC and SAML protocols correctly (aka the front-channel sign-out). Authway can be configured (requires setting on the instance done by IRM) for automatic re-direct which will re-direct the user back to the application when front-channel sign-out is done. If the applications that the user is signed in to, does not have any front-channel URLs registered the redirect will be performed without showing Authway signed out page.

Subsections of User sign-out

Trigger OpenId Connect Sign-out

An application (relying party) that want to initiate a single-sign-out will do that through the library used for OIDC. If the library doesn’t support sign-out this is possible to trigger from the application anyway by making a re-direct (HTTP GET) to a URL that will be something like this:

GET https://instance.irmciam.se/connect/endsession?
post_logout_redirect_uri={URL TO RETURN USER TO AFTER SIGN OUT}&id_token_hint={ID_TOKEN]&
state={OPTIONAL STATE RETURNED TO CLIENT}
Parameter Requirement Description
post_logout_redirect_uri Recommended The URL where the user will be re-directed after a completing the sign-out. If parameter isn’t supplied a generic signed out message is displayed. The URL must be registered in application configuration for the application in Authway. If an exact match isn’t found the parameter will be ignored and the user will not be redirected back to the application in any way.
id_token_hint Recommended Indication of the user that should be signed out. This is required for post_logout_redirect_uri to be considered. If it isn’t supplied the user might be prompted to sign-out (for security reasons).
state Optional State information that is returned to the client when the user is re-directed after the sign-out. For example you can store information about current user session or a re-direct URL that the user should be returned to within the application after being returned to the post_logout_redirect_uri.

Full specification: OpenID Connect RP-Initiated Logout 1.0

C# example

In this example the application saves information about re-directing the user to Index action after a complete sign-out. We recommend that the local re-direct URL allows anonymous access, or otherwise a new sign-in will be triggered as a result of the sign-out and that will be a strange user experience.

public async Task SignOut()
{
    await HttpContext.SignOutAsync();

    string returnUrl = Url.Action(nameof(Index));
    await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme,
                new AuthenticationProperties { RedirectUri = returnUrl });
}

Handle OIDC back-channel notification

The URL registered in Authway for back-channel notification when a user signs out will receive an HTTP POST with a logout token (JWT). This JWT token must be correctly validated by checking at least these requirements:

  1. The token is signed by the keys published by Authway (can be found through metadata).
  2. The issuer should be the Authway instance (exact value for issuer is also available in metadata).
  3. The audience should be the client id of the application receiving the notification.
  4. A sub claim that uniquely identifies the user should be present.
  5. A sid claim should be present if the application settings is to include session index.
  6. A nonce claim should be present.
  7. A events claim should be present and it should contain a http://schemas.openid.net/event/backchannel-logout event.

By using the sub claim and optionally the sid the application should invalidate the user session. How this is done will differ depending on platform/application.

.NET Sample

A sample for .NET can be found here.

Subsections of Access control

Role-based access control

It’s common to use role-based access control, so even though it we recommend you to use permission-based access control, we will show what needs to be done for the application to check if a user belongs to a role (called group in Authway). To include the groups a user belongs to, you are required to add roles scope to your request.

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 roles
          &state=YOUR_STATE

When doing so you will recieve one role claim for each group the user belongs too.

Even though this is straightforward to start with, there are several challenges with the role-based model that often result in many changes related to access control. For many features, it is common that many groups should have access, leading to very large if-statements. In a multi-tenant system (like the IdP, where each customer, reseller, partner, your own organisation itself, etc., is its own tenant), it’s also challenging to guarantee the names of the groups. For this, there is a concept of built-in group in IdP that cannot be renamed, and this must be used for role-based access control.

Example of using role-based access control

C# example

To include the roles a user belongs to, an additional scope and a claim-action must be added to the AddOpenIdConnect configuration:

options.Scope.Add("roles"); //Authway groups
options.ClaimActions.MapJsonKey("role", "role");

options.MapInboundClaims = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
    NameClaimType = "name", //Or your preferred claim for the user name
    RoleClaimType = "role"
};

With this additional configuration, claims of the type “role” will be retrieved, and since we had set RoleClaimType to “role” above, it means that the standard ways to check role membership will now work:

if (User.IsInRole("admin"))
    ...

eller:

[Authorize(Roles = "admin")]

Tips: IdentityModel is a package that has constants for many Claims which is a better way than using hardcoded strings as the examples above.

Permission-based access control

A system is built up of one or several functionalities. These functionalities are stable over time, meaning they exist as long as they are in the system, and they are never affected by organizational changes. Therefore, Authway has a concept of defining functionalities to which groups (or users) are granted access.

It is up to the system to define which functionalities need access control:

  • The system is a single functionality, meaning you either use the system or you don’t. It can also sometimes be divided into the right to read or the right to create/update/delete (administer).
  • One functionality per screen in the system.
  • One functionality for individual buttons in the system. An example could be a report, where one might have access for viewing (equivalent to a screen), but additionally has an extra functionality in the form of Export, where the content of the report can be exported. This could be more sensitive and therefore something one might want to control access for. In this case, you set up a function for “View Report X” and a function for “Export Report X”.

Functionalities can be managed directly in the Authway UI (User Interface), provided one has access to the “Manage Modules” functionality.

To include the permissions a user has, you are required to add perms scope to your request.

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 perms
          &state=YOUR_STATE

When doing so you will recieve one perm claim for each functionality the user has access too.

Permission-based access control with .NET

Permission-based access control can be used just as easily in .NET as role-based access control. To enable this, we need to replace one of the build-in services and create an extension method. Below is example code for .NET how to implement permission-based access control.

To demonstrate how this can be used, we have defined a single functionality for an Example application, which is “Manage Order,” and we have given it the authorization name “Example.ManageOrder” in Authway. For this, we define the following additions in the Constants class:

public static class Constants
{
    public static class Permissions
    {
        public const string ManageOrder = "Example.ManageOrder";
    }
}

Now we need to expand/change which scopes are to be retrieved from the IdP and simultaneously define claim actions in the AddOpenIdConnect configuration:

options.Scope.Add("perms"); //Authway permissions
options.ClaimActions.MapJsonKey("perm", "perm"); //Authway permission claim

To make it easy to check if a user has authorization for a function in the system, for example, the following extension method can be created:

public static class PrincipalExtensions
{
    public static bool Authorize(this ClaimsPrincipal principal, string permission)
    {
        return principal.Identity.IsAuthenticated && principal.HasClaim(c => c.Type == "perm" && c.Value == permission);
    }
}

With the extension method in place we can easily show/hide the link to Manage order page (in _Layout.cshtml) based on the user’s access:

@if (User.Authorize(Constants.Permissions.ManageOrder))
{ 
<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="ManageOrder">ManageOrder</a>
</li>
}

Certainly, we can check if a user has access in other code in the same way. However, it would be nice if we could use the Authorize attribute as well, so we could do something like this in our OrderController:

[Authorize(Constants.Permissions.ManageOrder)]
public IActionResult Index()
{
    return View();
}

In .NET Core/.NET 5 and later, Microsoft has finally realized that role-based access control might not be ideal and, as a result, introduced an authorization system based on policies (read more here: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies). To make it super easy to use our permission-based access control, we will create a custom PolicyProvider that automatically generates a policy to check authorization for a functionality, assuming the application hasn’t already defined a policy with that name:

/// <summary>
/// A policy provider that translates a policy name to a required permission with the same name, if not 
/// <see cref="DefaultAuthorizationPolicyProvider"/> already has provided a policy.
/// </summary>
public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    /// <summary>
    /// Creates a new instance of <see cref="PermissionAuthorizationPolicyProvider" />.
    /// </summary>
    /// <param name="options">The options used to configure this instance.</param>
    public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
    {
    }

    /// <summary>Gets the default authorization policy or a permission based policy if no default is configured.</summary>
    /// <returns>The authorization policy.</returns>
    public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        var policy = await base.GetPolicyAsync(policyName);

        if (policy == null)
        {
            policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .RequireClaim("perm", policyName)
                .Build();
        }

        return policy;
    }
}

Finally we need to tell .NET to use our PolicyProvider instead of DefaultAuthorizationPolicyProvider, and we do this like any other configuration in Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    //Add policy provider that generates a policy for each permission (if a policy is not found)
    services.AddTransient<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
}

With that configuration, we can now use the Authorize attribute as mentioned above.

Subsections of Consume an API

Machine-to-Machine (M2M) authentication

There are many scenarios where applications, such as CLIs, or Backend services, need to call other APIs and when these APIs requires an access token the application must be able to authenticate itself (if the call isn’t done on behalf of a user). The Client Credentials Flow (defined in OAuth 2.0) allows an application to exchange its credentials, commonly a Client ID and Client Secret for an access token.

Exchange Client Credentials for an Access Token

To exchange the client credentials for an access token the Token endpoint is used.

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

grant_type=client_credentials
 &client_id=THE_CLIENT_ID
 &client_secret=THE_CLIENT_SECRET
 &scope=THE_SCOPES_REQUIRED_TO_CALL_THE_API

C# Example

In this example code IdentityModel is used to simplify the code.

var client = new HttpClient();
var tokenRequest = new ClientCredentialsTokenRequest
{
     Address = "https://YOURINSTANCE.irmciam.se/connect/token",
     ClientId = "THE_CLIENT_ID",
     ClientSecret = "THE_CLIENT_SECRET",
     Scope = "THE_SCOPES_REQUIRED_TO_CALL_THE_API"
};
                
var response = await client.RequestClientCredentialsTokenAsync(tokenRequest);
                
if (response.IsError) throw new Exception(response.Error);
                
var token = response.AccessToken;

Use the Access Token

After retrieving the access token it should be added in the Authrozation HTTP header on all calls to the API:

Authorization: Bearer THE_ACCESS_TOKEN

The token is typically valid for a while (in many scenarios 1 hour), so re-use the token in multiple calls instead of retrieving a new for each call. This is extra important when many calls are done, since this will otherwise cause unnecessary load on Authway.

Call an API

It is common for a system/API to need to call another API within the same organization. These APIs are often protected in the same way, requiring a Bearer token for the request to be allowed. A Bearer token is sent in the HTTP header Authorization: Authorization: Bearer [ACCESS TOKEN].

The first decision to make is what the Access Token you’re sending should represent: a user or a system. When sending a token on behalf of a user, it’s referred to as Delegation, while when the system makes the call, it follows the Trusted Subsystem pattern.

Delegation: Making a call on behalf of a user

Delegation can be handled in two different ways. One common approach is what is sometimes called “poor man’s delegation,” which involves simply sending the user’s token directly to the underlying API. The other option is for the system making the call to perform a “token exchange” (OAuth Token Exchange specification, RFC 8693).

Regardless of the chosen model, it is necessary for your web application/API to access the user’s token. To enable this, .NET needs to store the token, which is done with the following configuration code in Startup.cs:

.AddOpenIdConnect(options =>
{
    ...
    options.SaveTokens = true;
    ...
});

Assuming this configuration is in place, it is possible to retrieve the user’s access token as follows:

httpContext.GetTokenAsync("access_token");

Poor mans delegation

The biggest advantage of this variant is that it is very easy to use. The only requirement is that all the API scopes that need to be used are included when the user’s access token is created. The downside of this solution is that if this access token falls into the wrong hands, it provides access to a larger attack surface than if it only gives access to a single API scope.

Typically, for this solution, you want to add the user’s access token to the requests made via HttpClient. A good way to handle this is to create a DelegatingHandler whose task is to do this, and configure HttpClient to use it. An implementation could look like this (available in IRM.Extensions.Http):

/// <summary>
/// A <see cref="HttpClient"/> handler that that adds a bearer token authentication header to the http request.
/// </summary>
public class BearerTokenAuthenticationHandler : DelegatingHandler
{
    private readonly IResolveBearerToken _resolveBearerToken;

    /// <summary>
    /// Creates a new instance of <see cref="BearerTokenAuthenticationHandler" />.
    /// </summary>
    /// <param name="resolveBearerToken">The <see cref="IResolveBearerToken"/> used to get a bearer token.</param>
    public BearerTokenAuthenticationHandler(IResolveBearerToken resolveBearerToken)
    {
        _resolveBearerToken = resolveBearerToken ?? throw new ArgumentNullException(nameof(resolveBearerToken));
    }

    /// <inheritdoc />
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _resolveBearerToken.GetTokenAsync(request, cancellationToken).ConfigureAwait(false));
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

The code above provides general support for adding a Bearer token and delegates the logic to retrieve the token itself to a class that implements IResolveBearerToken (also available in IRM.Extension.Http). For the “poor man’s delegation,” the implementation could look like this:

/// <summary>
/// Gets an access token from the current <see cref="HttpContext"/>.
/// </summary>
public class HttpContextBearerTokenResolver : IResolveBearerToken
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ILogger<HttpContextBearerTokenResolver> _logger;

    /// <summary>
    /// Creates a new instance of <see cref="HttpContextBearerTokenResolver" />.
    /// </summary>
    /// <param name="httpContextAccessor">The <see cref="IHttpContextAccessor"/> used to get the <see cref="HttpContext"/> where the access token is retrieved from.</param>
    /// <param name="logger"></param>
    public HttpContextBearerTokenResolver(IHttpContextAccessor httpContextAccessor, ILogger<HttpContextBearerTokenResolver> logger)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <inheritdoc />
    public Task<string> GetTokenAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var httpContext = _httpContextAccessor.HttpContext ?? throw new InvalidOperationException("There is no HttpContext available which is required for HttpContextBearerTokenResolver.");
        _logger.LogInformation("Resolves bearer token by getting the access token from Httpcontext.");
        return httpContext.GetTokenAsync("access_token");
    }
}

Token Exchange

The current version of Authway does not have complete support for Token Exchange, but it can be easily added. In conjunction with that, certain implementation details can be discussed based on the desired objectives.

Trusted Subssytem: System-to-system

To use Client Credentials, you need to configure either a regular client (Configure an Application)(../configure-authway/configure-an-application) or an external system (Configure an External System)(../configure-authway/configure-an-external-system).

OpenID Connect provides the Client Credentials flow, designed for situations where one system needs to call another system. Typically, you require a client ID and a client secret to obtain an access token using these credentials. This access token typically contains protocol-specific claims. Since it doesn’t represent a user, common claims like sub, email, name, or, in Authway’s case, perm or role, are not present. When implementing an API, significant work is required to handle this, as it differs significantly from when a user calls the API. For example, regular authorization controls don’t work, and if you want to log who made a change and save the name/ID of the person performing the change, that information is not available either. Authway supports something called External Systems to address this challenge.

External Systems are clients with support for Client Credentials. In the background, an associated user is created with the same ID (sub) as the client ID. This enables treating the calling system similarly to a user.

Example to retrieve an access token according to Client Credentials

In the same way as for a user above, we use the BearerTokenAuthenticationHandler, but with a different implementation to extract the access token. This code has dependencies on the IdentityModel NuGet package.

/// <summary>
/// Gets a client credentials access token from an identity server.
/// </summary>
public class ClientCredentialBearerTokenResolver : IResolveBearerToken
{
    private readonly ClientCredentialBearerTokenResolverOptions _options;
    private readonly HttpClient _httpClient;
    private IMemoryCache _cache;
    private readonly ILogger<ClientCredentialBearerTokenResolver> _logger;

    private const string _cacheKey = "IRM.Extensions.Http.ClientCredentialBearerTokenResolver.Token";

    /// <summary>
    /// Creates a new instance of <see cref="ClientCredentialBearerTokenResolver" />.
    /// </summary>
    /// <param name="httpClient"></param>
    /// <param name="optionsAccessor"></param>
    /// <param name="logger"></param>
    /// <param name="cache"></param>
    public ClientCredentialBearerTokenResolver(HttpClient httpClient, IOptions<ClientCredentialBearerTokenResolverOptions> optionsAccessor, ILogger<ClientCredentialBearerTokenResolver> logger, IMemoryCache cache = null)
    {
        _options = optionsAccessor?.Value ?? throw new ArgumentNullException(nameof(optionsAccessor));
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        if (_options.CacheAccessToken && cache == null)
            throw new ArgumentNullException(nameof(cache));
        _cache = cache;
    }

    /// <inheritdoc />
    public async Task<string> GetTokenAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (_options.CacheAccessToken && _cache != null)
        {
            var cacheKey = $"{_cacheKey}:{_options.ClientId}";
            var token = await _cache.GetOrCreateAsync(cacheKey, async entry =>
            {
                var response = await GetTokenFromServerAsync(request, cancellationToken).ConfigureAwait(false);

                entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(response.ExpiresIn).AddSeconds(-20);
                return response.AccessToken;
            }).ConfigureAwait(false);

            return token;
        }
        else
        {
            var response = await GetTokenFromServerAsync(request, cancellationToken).ConfigureAwait(false);
            return response.AccessToken;
        }

    }

    private async Task<TokenResponse> GetTokenFromServerAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var tokenUrl = await GetTokenUrl(_options.Authority).ConfigureAwait(false);

        var tokenRequest = new ClientCredentialsTokenRequest
        {
            Address = tokenUrl,
            ClientId = _options.ClientId,
            ClientSecret = _options.ClientSecret,
            Scope = _options.Scope
        };
        _logger.LogInformation("Tries to resolve bearer token by getting the access token from {tokenUrl} for client {clientId}.", tokenUrl, _options.ClientId);
        var response = await _httpClient.RequestClientCredentialsTokenAsync(tokenRequest, cancellationToken).ConfigureAwait(false);

        if (!response.IsError)
            return response;

        if (response.ErrorType == ResponseErrorType.Exception)
            ExceptionDispatchInfo.Capture(response.Exception).Throw();

        string error;
        if (response.ErrorType == ResponseErrorType.Http)
            error = response.HttpErrorReason;
        else
            error = $"Error: {response.Error}\nDescription: {response.ErrorDescription}";

        _logger.LogError(error);
        throw new Exception(error);
    }

    private async Task<string> GetTokenUrl(string authority)
    {
        var discovery = new DiscoveryCache(authority, () => _httpClient);
        var disco = await discovery.GetAsync().ConfigureAwait(false);
        if (disco.IsError)
        {
            if (disco.Exception != null)
                ExceptionDispatchInfo.Capture(disco.Exception).Throw();

            throw new InvalidOperationException($"Failed to get token url. Details: {disco.Error ?? disco.HttpErrorReason}.");
        }

        return disco.TokenEndpoint;
    }
}

External system

In a basic configuration of Authway, it is still the case that an access token in the client credentials flow does not contain any user claims such as sub, name, and perm because that’s how the standard is supposed to work. There is an option to extend Authway with support that ensures these claims are included even in client credentials, but this must be done by IRM.

Instead, we recommend that the API retrieves this additional information from the Auth module when calls to the API arrive, for example, in a ClaimsTransformation class. The advantage of this approach is that the API itself can control how fresh (updated) claims it wants by managing caching, compared to when all claims are packed in the access token with the same lifespan as the token. Additionally, the token becomes smaller, and there are other advantages that you can read about in Privacy and GDPR. The same documentation also includes code examples of how to retrieve more information."

Protect an API

To protect an API, you must have configured an API.

To secure the API, configure it to require an access token:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            options.Authority = authority; //Base URL to Idp
            options.Audience = apiResource;

            options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
            options.TokenValidationParameters.NameClaimType = JwtClaimTypes.Name;
            options.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role;

            // if token does not contain a dot, it is a reference token
            options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
        })
    // reference tokens
    .AddOAuth2Introspection("introspection", options =>
    {
        options.Authority = authority;

        options.ClientId = apiResource;
        options.ClientSecret = apiSecret;
        options.EnableCaching = true;
        options.CacheDuration = TimeSpan.FromMinutes(20);
        options.NameClaimType = JwtClaimTypes.Name;
        options.RoleClaimType = JwtClaimTypes.Role;
    });

We have used the IdentityModel.AspNetCore.OAuth2Introspection NuGet package. Read more about ForwardDefaultSelector.

If you have API and web application in the same project, cookies are likely the default scheme used for authenticating requests. However, if you need to make pure API calls from, for example, a server application, you may encounter 401 errors. To address this, you can add a ForwardDefaultSelector to your Cookies configuration. This can be configured to check if the Authorization header starts with “Bearer” and, if so, set “Bearer” as the scheme to be used.

In IRM.AspNetCore.Authentication, there are ready-made extension methods to perform the typical configuration mentioned above.

Privacy and GDPR

All digital solutions must comply with privacy-related regulations such as GDPR. Therefor privacy design must be part of the implementation of Authway by IRM.

Examples of concerns that must be considered for a privacy design:

  • Consent management: organisations must obtain explicit consents from individuals before collecting and processing data.
  • Access control: great handling of users access and permissions.
  • Data minimization: an organisation should limit the amount of personal data they collect and process to only what is necessary for a specific purpose.
  • Data protection: an organisation is required to implement appropriate technical and organizational measures to protect personal data from unauthorized access, disclosure, alteration, or destruction.

Authway has a lot of support to fulfil a good privacy design, but it must also be correctly used by the applications. Below are things to consider to protect your users and their privacy.

Scopes

A scope represents a set of claims (user attributes) that an application is requesting when authenticating a user. Scopes provides a mechanism for applications (and APIs) to only receive the subset of user information that are necessary rather than receiving all of it.

Consider what claims is really needed and request just the scopes needed, so that the tokens will contain as little PII data as possible. Sometimes it is also worth re-configuring the scopes to contain fewer claims, in line with the user data your applications will use.

Authway also supports fetching additional scopes through a back-channel if necessary. There are several benefits with using a back-channel compared to request tokens with many claims:

  • Size. The tokens will be smaller which is often good since they are passed around a lot.
  • Security. The user data that is never put into the token can never be revealed.
  • Freshness of information. Tokens can sometimes be long-lived, but if the application (or API) uses a back-channel to retrieve additional scopes it will always get the latest updates.

User data in tokens

A JSON Web token (JWT) is a structured token that typically contains claims (data) about the user and some extra metadata. They are self-contained, signed for integrity of its data and the format is well defined so that an API can easily decode and verify the token without calling any other APIs. Since the user data is sensitive and subject to regulations, such as GDPR, we recommend the use of Reference tokens, so that access tokens are only a random string and therefor can’t reveal any PII data. This requires the APIs to support both JWT and reference tokens, but it is usually not so much extra work. It is also possible to put the logic to exchange the reference token for a JWT token in an API Gateway, reverse proxy or any other middleware.

The reference tokens is exchanged for a JWT token by calling the Introspection endpoint of Authway. For performance reasons the exchanged token can be cached until it expires. This solution is fully compliant with OAuth 2 standard so no proprietary solutions is required for either the application or the API to use reference tokens.

Never send the token to a web browser

Store user cookies on the server

Application unique user identifiers

Export personal data

Authway has pre-build support for downloading a file with the personal data that exists in the service. This file can be reached from {auth domain}/identity/manage/download, but it requires a signed in user. The same information can exported from the Person admin API. If a more customized export is need, all information could be gathered by calling our different admin APIs to fully customize how the information is exposed to the user.

Delete personal data

We have several automized clean-up jobs that remove personal data that can be enabled and configured.

The automated jobs will not be enough in many situations and complementary tools are necessary. The admin UI of course supports a manual remove of persons and their data which also can be used directly by your end-customers. Everything that can be done in the UI can be automated by calling our admin APIs.

One challenge with a shared service for users, is that it can be hard to know when a user can/should be removed. An example could be a user that has access to system A, B and C. When system B decides that the user should be removed (could be build in or decided by a centralized rule engine) there is no way for system B to decide if the user should be removed from Authway. To handle this situation Authway have a specialized API where system B can request to remove all permissions to the system for a user. This together with the clean-up job to remove users that has no permissions can solve this challenge.

API to remove all permissions for a module

HTTP DELETE /api/users/{userId}/permissions/modules/{moduleName}

If userId is unknown it is possible to search for user by username. The API will remove the user from all groups that has permissions that belong to the module. It will also remove user specific permissions that belongs to the module.

Signing keys

Authway uses public-key cryptography to sign ID tokens, access tokens and SAML assertions, so that an application (client) that uses Authway to authenticate users can trust the result. A signing key is a JSON web key (JWK) that consists of a public and privat key pair. The private key is used by Authway to create a signature that can be verified with the well-known public key.

By default, Authway uses RSA keys for RS256 signing algorithm.

A JSON web key set (JWKS) is a set of keys containing the public keys used to verify the signature in your application. The set can contain one or more public keys:

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "NOT THE REAL VALUE",
            "e": "NOT THE REAL VALUE",
            "n": "NOT THE REAL VALUE",
            "alg": "RS256"
        }
    ]
}

The JWKS can be fetched from the Identity Server at the relative URL .well-known/openid-configuration/jwks, but the preferred way to retrieve the URL for the JWKS is to read the jwks_uri from the metadata document (found at .well-known/openid-configuration). Within the set there can be one or more keys for one or more signing algorithms (if more than one algorithm is configured).

How it works

When a user signs in, Authway typically creates tokens with information about the signed-in user. The tokens are signed with the private key, before they are send to your application. To verify that the the token you retrieve is valid and issued by the Identity Server, your application uses the public key.

It is important that you use the jwks_uri endpoint to get the keys dynamically since the keys can change over time. Of course the keys should be cached, but it is a good practices to refresh the cash once every day.

Note that validation of the signature is only one aspect of validating a JWT and expiration, issued time, audience and issuer are examples of more necessary validation. We recommend the use of platform support for token validation is used since it is a complex and security critical process to do correct.

Key rotation

Key rotation is an Enterprise feature of Authway, but since this can change (both what is included in different price levels, but also what version is used) we strongly recommend each application to implement support for key rotation. The keys can also be changed in the case of a security breach.

It is a good practice to rotate the keys regularly and Authway rotates them every 90 days (by default, but it can be configured). The new key is announced 14 days in advance, and retained for 14 days after it expires. The first key in the list is the default key:

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "NOT THE REAL VALUE",
            "e": "NOT THE REAL VALUE",
            "n": "NOT THE REAL VALUE",
            "alg": "RS256"
        },
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "NOT THE REAL VALUE",
            "e": "NOT THE REAL VALUE",
            "n": "NOT THE REAL VALUE",
            "alg": "RS256"
        }
    ]
}

If the signature couldn’t be verified with the default key, you should try to verify the signature with the other key(s) for the same signing algorithm.

Signing algorithms

By default Authway uses RS256 signing algorithm, but it is possible to configure support for RS, PS and ES family of cryptographic signing algorithms. When multiple algorithms is supported is is possible to override the default on a per resource and client basis.

Tokens

There are primarily two types of tokens related to identity; ID tokens and Access tokens.

ID Tokens

ID tokens are JSON web tokens (JWT) that an application uses to retrieve information about a user. You shouldn’t use ID tokens when calling an API.

Access Tokens

Access tokens are used when calling an API to prove that the bearer of the token is allowed to call the API and what parts of the API is allowed to be used by the consumer.

Special Tokens

Refresh tokens is a special token, used to renew an access token without forcing the user to re-authenticate.

Subsections of Tokens

ID Tokens

The ID Token is conceptually the same as an identity card, with information about a user packaged as a JWT token. ID Tokens can be returned as a result when an application makes an OpenId Connect (OIDC) authentication request to the Identity Provider. The statements (called claims) of an ID Token are packaged in a simple JSON object (this is the Body of the JWT token).

  {
   "iss": "https://instance.irmciam.se",
   "sub": "f404a4ff-6037-4097-afa7-24b397239009",
   "tid": "d10a1624-6de7-4b50-b662-0f1cc6733edf",
   "aud": "APPLICATION CLIENT ID",
   "nonce": "VALUE",
   "exp": 1311281970,
   "iat": 1311280970,
   "auth_time": 1311280969,
   "amr": "pwd"
  }

An ID Token has these features:

  • Asserts the identity of the user, called subject in OIDC and represented with a sub claim.
  • Asserts the identity of the tenant, represented with a tid claim. This is not standardized or required in the protocol, but a claim that Authway always issues.
  • Specifies the issuing authority, represented with a iss claim.
  • Is generated for a specific application, called client in OIDC and represented with a aud claim.
  • Has an issue (iat claim) and expiration time (exp claim).
  • May contain other protocol claims like
    • A nonce that associates a client session with the token, which is used to mitigate replay attacks.
    • One or more authentication method reference (amr claim) that represents how the user was authenticated. There are standardized values that Authway uses if it exists.
  • May contain other claims about the user, like name and email, depending on what scopes was requested.

The ID token header, claims JSON and signature are encoded into a base 64 URL-safe string, so it can easily be passed around.

Considerations to make for ID Tokens

What claims end up in the ID Token is controlled by passing Scopes to the Authorization endpoint. The openid scope is required when requesting an ID Token, but other can be requested to retrieve more information about the user. In Authway the openid scope contains the sub and tid claims.

When requesting many scopes the returned ID Token will contain many claims, which can result in a large ID Token. The size of the token can effect how good it is to keep in a cookie or other state for the client application. It is good to keep the token as small as possible. It is possible to request more information about the user from the UserInfo endpoint instead. The downside of that alternative is that it results in more network requests which can affect the latency of the application during sign-in.

Access Tokens

Refresh Tokens

Refresh tokens are used to retrieve a new access token for a user without requiring the user to interactively authenticate again. A refresh token is typically long-lived (from days up to a month is common), but they enable the creation of short-lived (from minutes to hours) access tokens. This is good from security perspective since anyone that is in possession of an access token is allowed to call API:s that the token has access too and by giving them a short life-time the consequence of a stolen access token is less than if it is long-lived.

Get a refresh token

It is easy for the application to request a refresh token, since all that is necessary is to add the offline_access scope to the scope parameter list when making the authorization request.

Since it is a privileged operation to use a refresh token, the client must be configured to allow offline access. In client administration it is also possible to configure for how long a refresh token is valid and other settings.

Use a refresh token

The refresh token is used to retrieve a new access token (and/or ID token) for a user without re-authenticating the user. This should be done a short while before the access token expires (and not each time you need an access token) so that you always have an access token that are valid when calling the API.

To get a new access token, the refresh token is send to the token endpoint and the result is a token response containing a new access token, its expiration and potentially a new refresh token.

POST /connect/token
    client_id=client&
    client_secret=secret&
    grant_type=refresh_token&
    refresh_token=the_refresh_token

Security Consideration

Refresh tokens are extremally valuable (because they are long-lived) and must be carefully protected by the clients that are allowed to use them.

Refresh Token Rotation

By default Authway rotates the refresh token on each usage so that the refresh token only can be used once. This reduces the attack surface because there is a chance that a stolen token becomes unusable before the attacker have used it.

Since this is the default configuration for a client, it is important that the client takes this in consideration when using the refresh token. The client is responsible to synchronize the renewal between threads (so the refresh token isn’t used multiple times) and since the second use of a refresh token will result in a response with status code 400, it is a risk that the client invalidates the user session.

For .NET developers we recommend you to use Duende Access Token Management which has support for many token management tasks.

Revoke Tokens

Subsections of Integrations

Admin APIs

Everything that can be done in Admin UI (and a little bit more) can also be done through API calls. This makes it possible to embed administration into other applications and completely customize the UI for administration.

Some of the APIs are limited to a single tenant, for example it is only possible to handle users for a single tenant. The following API:s are bound to a tenant:

  • Person
  • Users
  • Groups
  • External systems
  • Organisations (depending on permissions)

For external systems and users that have the special permission (manage organisations) to configure tenants it is possible to control which tenant each API call should be handled for by passing the X-IRM-TenantId HTTP Header with the unique identity (UUID) as value.

Within the Users APIs there are special /me endpoints which requires an access token of an actual end-user to be used. Since they represent an actual user, there is no need to pass any Tenant Id.

All other APIs are configuration APIs and instead requires very specific permissions to be used.

Important information about the unique identifiers (GUID:s)

Many APIs allows you to pass a unique identifier in the form of a GUID (UUID), but we recommend you to not do that and instead let Authway create an optimized Id, which is returned in all responses to create APIs. If you need to pass the Id from your code we strongly recommend you to use an algorithm that creates identifiers optimized for the underlying database, or else the performance of the system will be negatively impacted. We can provide you with such algorithm for SQL Server.

Errors from the APIs

We try to use reasonable status codes for different situations in the API. When we return a body with error information, the body follows the Problem Details for HTTP APIs (RFC 7807) standard.

Status Code Description Details
400 Errors in the provided data. This will always have a problem details body with more information about the error.
401 Failed to authenticate the call.
403 The user or client calling the API is missing the required permission.
404 The URL is not matching any API or the id:s in the URL does not match any existing object. This will often have a problem details body with more information about the error.
500 An internal server error. Of course we make our best to not end up with a 500 error, but it could for example be a dead lock situation or that we for some reason couldn’t reach the database at all and so on.
501 Not implemented or supported. Very uncommon, but there are scenarios where you can call the API with values that are not incorrect, but still not supported. Another situation is when trying to use an API for a feature that is not part of the bought service. In those cases, we have chosen to give a 501 instead of a 400. This could happen during development, but should never be a case in production if the consumption of the API have been tested correctly, but 400 errors could exist in production and be caused by end-user data.
503 Infrastructure problem. Most likely temporarily.

Subsections of Admin APIs

Deprecated APIs

Below is a list of APIs that will be deleted in a future version together with suggestion what to do (if there are any).

2024-10-05

  • api/tenants/{tenantId}/modules/{moduleId}/buy is replaced with api/tenants/{tenantId}/modules/{moduleId}/activate
  • api/users/{userId}/invitationlink resp. api/users/{userId}/resetlink will retire { "initationLink"="https://....." } respectively { "resetLink"="https://....." } and instead now use a shared model:
{
    "link"="https://....."
    "validTime"="3.00:00:00"
}

2024-07-10 (removed from 1.2.139, 2024-10-16)

This is a breaking change and we have contacted each customer to ensure a quick timeline until this is fixed.

  • api/persons will be bound to the current user tenant (or the tenant in HTTP header X-IRM-TenantId). Instead the CreatePersonViewModel contained a TenantId which is still there for compatibility reasons, but should not be used anymore.

2023-05-15 (removed from verison 1.2.110, 2024-09-23)

  • api/tenants/{tenantId}/module is replaced with api/tenants/{tenantId}/modules
  • api/group/* is replaced with api/groups/*
  • api/groups/{groupId}/user is replaced with api/groups/{groupId}/users

OAS 3.0

Events API

Authway has events for many of the things that happens in the service. You can tap into these events and write your own logic by registering a webhook or by using our Event API. Note that webhook feature is only available in the Enterprise edition of the service.

Common Properties in all events

Each event payload also contains properties unique to the event. You can find the unique properties in the individual event documentation.

Name Type Description
AggregateId UUID The unique identifier of the aggregate (information entity) that the event happended for.
OwnerId UUID The unique identifier of the owner (tenant) that the event belongs to.
EventId UUID The unique identifier for the event. Can be used for idempotency, logging and more.
Occured DateTime The date and time (in UTC) when the event occurred.
CausedByPersonId UUID The unique identifier of the user that caused the event (if any).
CausedBy string The name of the user that caused the event (if any).
TraceId string The trace id that can be used to trace different actions that is going on in a system.

IpAddressLocation Properties

Depending on environment and configuration some events will include location information based on the IP address. Sources for this information can also be different when hosting your-self compared to Authway IDaaS.

Name Type Description
CountryCode string The two-letter country code (3166-1) representing the country.
Country string The name of the country.
Region string The name of the region.
City string The name of the city.
Latitude decimal The latitude.
Longitude decimal The longitude.

When an IpAddressLocaiton exist in the event, we garuantee that the CountryCode and Country is set, but the other properties will depend on information availability in the source.

Available Events

Organisation Events

To retrieve all organisation events you use the topic “organisation”.

Person Events

To retrieve all organisation events you use the topic “person”.

User Events

To retrieve all organisation events you use the topic “user”.

Organisation module Events

To retrieve all organisation module events you use the topic “organisationmodule”.

Module Events

To retrieve all module events you use the topic “module”.

Subsections of Events API

Subsections of Organisation Events

OrganisationClaimAdded

An organisation got an organisation claim added. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.organisationclaimadded”.

Name Type Description
AggregateId UUID The unique identifier of the organisation.
ClaimType string The type of claim.
ClaimValue string The claim value.

OrganisationClaimRemoved

An organisation got an organisation claim removed. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.organisationclaimremoved”.

Name Type Description
AggregateId UUID The unique identifier of the organisation.
ClaimType string The type of claim.
ClaimValue string The claim value.

OrganisationCreated

An organisation is created. It is a new tenant if the ParendId does not have any value (and GroupMotherId and AggregateId is equal). To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.organisationcreated”.

Name Type Description
AggregateId UUID The unique identifier of the organisation.
GroupMotherId UUID The id of the group mother (root organisation) in the organisation tree. For the group mother, this value will be the same as Id.
ParentId UUID The identity of this tenants parent, if any.
Name string The name of the organisation
IdentityNumber string The identity number for the organisation.

TrustedDomainRemoved

An organisation got a trusted domain removed. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.trusteddomainremoved”.

Name Type Description
AggregateId UUID The unique identifier of the organisation.
Domain string The domain that was removed.

TrustedDomainAdded

An organisation got a trusted domain added. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.trusteddomainadded”.

Name Type Description
AggregateId UUID The unique identifier of the organisation.
Domain string The domain that was added.

OrganisationUpdated

An organisation is updated. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.organisationupdated”.

Name Type Description
AggregateId UUID The unique identifier of the organisation.
Name string The name of the organisation
IdentityNumber string The identity number for the organisation.

OrganisationDeleted

An organisation is deleted. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.organisationdeleted”.

Name Type Description
AggregateId UUID The unique identifier of the organisation.

Subsections of Person Events

PersonCreated

A person is created. It is common for UserCreated to be created at the same time, but it is possible to create a person who are not a user. To retrieve only this event you use the topic “person/irm.aspnetcore.identity.events.personcreated”.

Name Type Description
AggregateId UUID The unique identifier of the person.
OrganisationId UUID The id of the organisation that the person is added to.
FirstName string The first (given) name of the person.
LastName string The last (family) name of the person.
Email string The email for the person. This can be a different email than the username and/or user email.

PersonDeleted

A person is deleted. When a person is deleted, so is all user and person events for that person. The metadata is preserved, but no payload data will be available again. To retrieve only this event you use the topic “person/irm.aspnetcore.identity.events.persondeleted”.

Name Type Description
AggregateId UUID The unique identifier of the person.

PersonUpdated

A person is updated. To retrieve only this event you use the topic “person/irm.aspnetcore.identity.events.personupdated”.

Name Type Description
AggregateId UUID The unique identifier of the person.
OrganisationId UUID The id of the organisation that the person belongs to.
FirstName string The first (given) name of the person.
LastName string The last (family) name of the person.
Email string The email for the person. This can be a different email than the username and/or user email.

Subsections of User Events

UserCreated

A user is created. This can happen multiple times for the same user id, since it is possible to delete only the user and then re-create the user. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.usercreated”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
Username string The username which is unique within the tenant, but can exists for multiple tenants.
Email string The email address of the user. This is typically the same as Username, but the service can be configured to use PhoneNumber or any username and in thoose cases it can differ.
EmailConfirmed bool True if the email is confirmed; otherwise false.
PhoneNumber string The phone number of the user. In a default configuration phone number is not visible and collected, so it is common for this to be null.
PhoneNumberConfirmed string True if the phone number is confirmed; otherwise false.
ValidFrom DateTime The date and time (in UTC) when the user is valid (aka the earliest point in time when the user is allowed to sign-in).
ValidTo DateTime The date and time (in UTC) when the user is valid (aka the latest point in time when the user is allowed to sign-in). Commonly null.
IsSystemUser bool True if this user represents a system (aka an external system); otherwise false.
SendInvitation bool True if an invitation will be send to the user; otherwise false.
AdditionalInvitationParameters string Additional parameters that should be added to the invitation link. Typically it can contain for example a client id to brand the create account page for a specific client.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserActivated

A user account have been activated, which can be immediately or at Valid from. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.useractivated”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).

Note that there can be a delay until the event is pushed, but it will always be pushed in correct order.

UserUpdated

A user is updated. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userupdated”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
Username string Obsolete.Use UserUsernameChanged event to handle changes of the username instead.
Email string The email address of the user. This is typically the same as Username, but the service can be configured to use PhoneNumber or any username and in thoose cases it can differ.
EmailConfirmed bool True if the email is confirmed; otherwise false.
PhoneNumber string The phone number of the user. In a default configuration phone number is not visible and collected, so it is common for this to be null.
PhoneNumberConfirmed string True if the phone number is confirmed; otherwise false.
ValidFrom DateTime The date and time (in UTC) when the user is valid (aka the earliest point in time when the user is allowed to sign-in).
ValidTo DateTime The date and time (in UTC) when the user is valid (aka the latest point in time when the user is allowed to sign-in). Commonly null.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserUsernameChanged

The username of a user is changed. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userusernamechanged”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
Username string The username which is unique within the tenant, but can exists for multiple tenants.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserDeleted

A user is deleted. The user can be deleted, without deleting the person, which also results in that it is possible to re-create the user with the same id. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userdeleted”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).

UserDeviceAdded

An user signed in from a new device. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userdeviceadded”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
DeviceId string The unique identifier of the device.
FromIpAddress string The IP Address of the user that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.

UserDeviceCountryAdded

An user signed in from a known device, but a new country. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userdevicecountryadded”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
DeviceId string The unique identifier of the device.
FromIpAddress string The IP Address of the user that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.

UserInvited

A user is invited to create an account. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userinvited”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserLoginAdded

An user added an external login to the account. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userloginadded”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
LoginProvider string The login provider added.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserLoginRemoved

An user removed an external login from the account. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userloginremoved”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
LoginProvider string The login provider removed.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserPasswordAdded

An user added a password. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userpasswordadded”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserPasswordChanged

An user changed the password. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userpasswordchanged”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserPasswordRemoved

An user removed the password from the account. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userpasswordremoved”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserRoleAdded

A user is added to a role. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userroleadded”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
NormalizedRoleName string The unique and normalized role name that the user was added to.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserRoleRemoved

A user is removed from a role. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userroleremoved”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
NormalizedRoleName string The unique and normalized role name that the user was removed from.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserSignInAssociated

A user has associated a sign-in from an invitation to create an account. This also means that the email address is confirmed. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.usersigninassociated”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
AuthenticationMethod string The type of authentication that the user used when associating an authentication method.
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserSignedIn

A user signed in. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.usersignedin”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
Kind int Indicates what kind of situation that caused the event. Can be one of these values: 0. Interactive sign-in where user is fully aware; 1. Automatic sign-in where the user is signed-in automatically by single-sign-on; 2. Refresh where an application uses a refresh token to re-new the user sign-in; 3. Impersonate which is when another user impersonates the user.
AuthenticationRequirement string The requirment of the authentication process for the user, for example “1FA” or “2FA”. This will only be set when Kind is 0 or 3.
AuthenticationMethod string The type of authentication that the user used when signing in. This will only be set when Kind os 0 or 3.
FromIpAddress string The IP Address of the user (or service) that caused the event. When kind is 2 (Refresh) this will be the IP Address of the server, not the user, since it is performed over a backchannel.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event. Will be the user’s web browser user agent for all Kind except when Kind = 2 (Refresh) which happens in a backchannel.
Metadata dynamic A dynamic object with additional data for the event.

More about Metadata

Metadata is a dynamic object that can contain different extra properties that might vary on Kind, protocol used or other factors.

When the event occurs as a result of using OpenId Connect it will include these:

Name Type Description
ClientId string The unique identifier of the client that asked to sign-in the user.
ClientName string The name of the client that asked to sign-in the user.

The JSON will be like this:

{
   ...
   metadata: {
      clientId: "UniqueClientId",
      clientName: "The perfect client"
   }
}

When the event occurs as a result of a user impersonating another user it will include these:

Name Type Description
ImpersonatedByUserId UUID The unique identity of the user impersonating the user.

The JSON will be like this:

{
   ...
   metadata: {
      impersonatedByUserId: "UniqueUserId",
   }
}

UserSignedOut

An user signed out. This event only happens when a user activly signs out, and not because of a timeout of a valid sign-in. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.usersignedout”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserSignInFailed

An user failed to sign in. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.usersigninfailed”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.
Reason numeric One of the following values indicating the reason to why the sign-in failed: 0. Invalid credentials, 1. Locked-out user, 2. Inactive user, 3. Impossible travel, 4. Module not activated for tenant or 5. Module is offline.
BreachedPasswordUsed bool true if a breached password have been used for this failed sign-in; otherwise false. If password wasn’t used or if breach detection is not configured this will be null.

UserLockedout

A user account have been locked (commonly because to many sing-in attempts). To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userlockedout”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserUnlocked

A user account have been un-locked. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userunlocked”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserDeactivated

A user account have been de-activated, which is at Valid to. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userdeactivated”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).

Note that there can be a delay until the event is pushed, but it will always be pushed in correct order.

UserReactivated

A user account have been activated again after beeing de-activated, which can be immediately or at Valid from. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userreactivated”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).

Note that there can be a delay until the event is pushed, but it will always be pushed in correct order.

UserConfirmedEmail

An user confirmed the email address. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userconfirmedemail”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

UserConfirmedPhoneNumber

An user confirmed the phone number. To retrieve only this event you use the topic “user/irm.aspnetcore.identity.events.userconfirmedphonenumber”.

Name Type Description
AggregateId UUID The unique identifier of the user (always the same as the person id for end-users).
FromIpAddress string The IP Address of the user (or service) that caused the event.
IpAddressLocation IpAddressLocation Ip address information if available.
UserAgent string The user agent string from the browser (or service) that caused the event.
Metadata dynamic A dynamic object with additional data for the event.

Subsections of Organisation module Events

ModuleActivatedForOrganisation

An organisation got a module activated. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.moduleactivatedfororganisation”.

Name Type Description
AggregateId UUID The unique identifier of the activated module and organisation combination.
OwnerId UUID The unique identifier of the organisation.
ModuleId UUID The unique identifier of the module.

ModuleInactivatedForOrganisation

An organisation got a module in-activated. To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.moduleinactivatedfororganisation”.

Name Type Description
AggregateId UUID The unique identifier of the in-activated module and organisation combination.
OwnerId UUID The unique identifier of the organisation.
ModuleId UUID The unique identifier of the module.

ModulePayedForOrganisation

An organisation got a module set as payed (after beeing unpayed). To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.modulepayedfororganisation”.

Name Type Description
AggregateId UUID The unique identifier of the payed module and organisation combination.
OwnerId UUID The unique identifier of the organisation.
ModuleId UUID The unique identifier of the module.

ModuleUnpayedForOrganisation

An organisation got a module set as un-payed (the effect is that users from that organisation can’t sign-in until set as payed again). To retrieve only this event you use the topic “organisation/irm.aspnetcore.identity.events.moduleunpayedfororganisation”.

Name Type Description
AggregateId UUID The unique identifier of the unpayed module and organisation combination.
OwnerId UUID The unique identifier of the organisation.
ModuleId UUID The unique identifier of the module.

Subsections of Module Events

ModuleWentOffline

A module went offline. To retrieve only this event you use the topic “module/irm.aspnetcore.identity.events.modulewentoffline”.

Name Type Description
AggregateId UUID The unique identifier of the module.

ModuleWentOnLine

A module went online. To retrieve only this event you use the topic “module/irm.aspnetcore.identity.events.modulewentonline”.

Name Type Description
AggregateId UUID The unique identifier of the module.

FunctionalityDeleted

A functionality is deleted. When a functionality is deleted, the permission is also deleted from Group templates, Groups and External Systems. To retrieve only this event you use the topic “module/irm.aspnetcore.identity.events.functionalitydeleted”.

Name Type Description
AggregateId UUID The unique identifier of the module.
FunctionalityId UUID The unique identifier of the funcitonality.
Permission string The unique identifier of the permission.

Webhooks

Authway uses webhooks to automatically notify your application any time certain changes happens in Authway.

Webhooks can be set up per event stream, where a stream is responding to a major information object for examples person, user or organisation. It is possible to set up multiple webhooks for a single stream and naturally it is possible to set up webhooks for different streams. The webhooks are grouped in a webhook system. The webhook system will typically be one for the consuming application. If one webhook is failing for a system, all webhooks for that system is stopped, which can affect how you choose to group your webhooks.

By default you will only be able to receive events that is owned by the same tenant as the Webhook is registered for.

Handling events

Choose event to recieve

When configuring a webhook, you can use the API to choose which events will send you payloads. This is done by setting a topic to the stream or a specific event. Only subscribing to the specific events you plan on handling limits the number of HTTP requests to your application. You can also subscribe to all current and future events. By default, webhooks are only subscribed to the future events. You can change the topic at any time, but for an existing webhook, it will only affect future events.

Advanced Topics

A topic is flexible and you can combine more than one “thing” in a topic by separating each identifier with “,”. For example:

Topic Description
person, user/irm.aspnetcore.identity.events.usersignedin All events for person and UserSignedIn event.
user/irm.aspnetcore.identity.events.usersignedin, user/irm.aspnetcore.identity.events.usersignedout, user/irm.aspnetcore.identity.events.usersigninfailed The three events UserSignedIn, UserSignedOut and UserSigninFailed.
person, user All events for both person and user.

Respond immediately to events

When receiving an event to your webhook, it is strongly recommended to respond with a 200 Ok (or another 2XX status code) as quickly as possible. Failing to do so might trigger re-tries which can increase the load of your application more than necessary.

A common pattern to handle webhook events effectivly is to add the event body on a message queue and immediately respond with a success status code. The internal queue can then be processed by a background worker.

Re-tries of failed events

For a configured webhook we guarantee that subscribed events are delivered at least once and in correct order (per webhook). If we get a timeout, a network failure or if we receive a response with status code 408 or any 5XX we’ll retry to send the payload again. Authway uses an exponential backoff scheme when doing re-tries and if all re-tries fails the webhook system will stopped. When this happens we will notify you by the registered e-mail address. It is your responsibility to re-start a stopped webhook system when your application is ready to handle the events again.

If Authway is causing the failure, we will also re-start the webhook system after we have corrected the issue.

Ignoring duplicate events

It is possible to receive the same event more than once, so we recommends that you handle webhook events using idempotent operations. One way of doing this is logging the EventId of the events that you have processed and ignoring subsequent requests with the same EventId.

Order of events

Authway guarantees that events within a webhook is delivered in the same sequence that they are happening. Between webhooks there are no guarantees of ordering, so your logic should handle cases where events are delivered out of order. If you create separate webhook systems, the ordering of the events between these systems is also not guaranteed.

Securing webhooks

Validating events are from Authway

Since the webhook receiver is available on the public Internet it is very important that you validate the incoming request before processing it. We recommend that you start by checking that all three custom HTTP headers are available. From security perspective the X-IRM-Signature header is the important one. Use your secret (that you used during registration) to calculate a HMAC-SHA256 signature of the received body.

C#:

using System;
using System.Security.Cryptography;
using System.Text;

class WebhookSignature
{
   public static string ComputeSignature(string data, string secret)
   {
       HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
       byte[] stringBytes = Encoding.UTF8.GetBytes(data);
       byte[] hashedValue = hmac.ComputeHash(stringBytes);
       return Convert.ToBase64String(hashedValue);
   }

   public static void Main (string[] args)
   {
       Console.WriteLine(ComputeSignature("messageBody", "apikey"));
   }
} 

PHP:

function compute_signature($data, $secret) {
    return base64_encode(hash_hmac('sha256', $data, $secret, true));
}

echo(compute_signature('messageBody','apikey')); 

Require authentication

Authway supports Basic authentication when calling your webhook. You provide the username and password when registering the webhook.

Obfuscating webhook URL

A minor improvment in the security of webhooks is to make the endpoints harder to guess. This can be done by adding a series of random numbers and letters to your endpoint URL. For example https://api.yourapplication.com/webhooks/j490smlkfs034jld94jlae045 is harder to guess than https://api.yourapplication.com/webhooks.

Testing Webhooks

It is not necessary for you to immediately create och deploy a service that can receive webhook payloads. Instead we recommend you to start with ay of the service on Internet that allows you to receive HTTP POST calls, for example https://webhook.site/. That service makes it really easy to get something up and running, and you can easily see the webhook payload directly in your browser.

To start receiving webhooks you must first start (activate) the webhook system by calling api/systems/{systemId}/start. Trigger some events in the source system and you should soon see POST arriving in the browser.

You can also stop the pushing of new events by calling api/systems/{systemId}/stop. If you want to simulate that the system have been stopped because of a failure you can use the same API. Use the source system to create some more events and then start the system again, and you should see all events be posted until it catches up to the last existing event.

You can also get information about the last date/time and event id that have been successfully posted to your webhook by making a get request to api/systems/{systemId}.

Exposing your Local Machine to the Internet

Since all webhooks are pushed over Internet, you’ll have to expose your local machine somehow, if you want to receive webhooks during development. We recommend that you use for example Ultrahook or ngrok to do that.

After configuring a service you’ll have to reconfigure your webhook system to receive payloads on your new URL.

Subsections of Webhooks

Webhook Events and Payload

Webhook Events and Payload

Webhook Common Properties in the Payload

Each webhook event payload also contains properties unique to the event. You can find the unique properties in the individual event documentation.

Name Type Description
AggregateId UUID The unique identifier of the aggregate (information entity) that the event happended for.
OwnerId UUID The unique identifier of the owner (tenant) that the event belongs to.
EventId UUID The unique identifier for the event. Can be used for idempotency, logging and more.
Occured DateTime The date and time (in UTC) when the event occurred.
CausedByPersonId UUID The unique identifier of the user that caused the event (if any).
CausedBy string The name of the user that caused the event (if any).
TraceId string The trace id that can be used to trace different actions that is going on in a system.

Delivery Headers

The HTTP POST payloads send to your webhook’s configured URL endpoint will contain the following custom HTTP headers:

Header Description
X-IRM-Signature A base64 encoded HMAC-SHA256 signature of the body, signed with your API key (secret).
X-IRM-Topic The topic that you registered for this webhook encoded as base64 (UTF8).
X-IRM-EventType A base64 (UTF8) encoded full name of the event, guaranteed to be unique.

Also, the User-Agent for the requests will be IRM-Webhook.

Subsections of Migration

Migration of Users With Passwords

We support migrations of users and their passwords if we can implement the same hash algorithm that is currently used for creation and validation of their passwords. After an old/migrated password is validated the password will be re-hashed to use our modern hashing solution to protect the passwords.

If the hashing alorithm is unknown or is not possible to implement, users can still be migrated and an e-mail can be send to all users with a link where they can create a new password (or use another sign-in alternative). We can also implement more customized migration strategies based on you requirements.

Existing hash algorithm support

We currently have existing support for MembershipHasher (if you are migrating from ASP.NET Membership) and SimpleMembershipHasher (if you are migrating from ASP.NET Simple Membership).

Other migration support

If you have admin UI:s that uses ASP.NET Roles, we have a custom AuthwayRoleProvider that can be used with small modifications on existing code to talk to our admin API:s. Just let us know and we’re happy to share this with you.

The changes needed is likely to handle the fact that Authway is multi-tenant, but Roles (and Membership) have no concept of multi-tenancy. This might force code changes to use are extended methods to pass in the TenantId that should currently be handled or configure a single TenantId that the existing admin UI should work against.