Home > Mobile >  .Net Framework Multi-Tenant OIDC Authentication
.Net Framework Multi-Tenant OIDC Authentication

Time:10-25

We have 3 applications (Desktop, Mobile, Excel Addon), which means 3 client IDs, that need to call our APIs.

Is it possible to configure our API to work with those 3 client IDs? Here's the current config :

            string OidcAuthority = Config.OidcAuthority;
            string OidcRedirectUrl = Config.OidcRedirectUrl;
            string OidcClientId = Config.OidcClientId;
            string OidcClientSecret = Config.OidcClientSecret;

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            var oidcOptions = new OpenIdConnectAuthenticationOptions
            {
                Authority = OidcAuthority,
                ClientId = OidcClientId,
                ClientSecret = OidcClientSecret,
                PostLogoutRedirectUri = OidcRedirectUrl,
                RedirectUri = OidcRedirectUrl,
                ResponseType = OpenIdConnectResponseType.Code,
                Scope = OpenIdConnectScope.OpenId                    
            };
            app.UseOpenIdConnectAuthentication(oidcOptions);

Edit:

Here's the code we had when we were using oauth2 and Auth0 services.

            var domain = Config.Domain;
            var apiIdentifier = Config.ApiIdentifier;
            var keyResolver = new OpenIdConnectSigningKeyResolver(domain);

            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = apiIdentifier,
                        ValidIssuer = domain,
                        IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => keyResolver.GetSigningKey(kid)
                    }
                });

I know I have to use something similar but I have 3 possible apps that could generate a token, which means 3 possible audiences... How should I do it?

Edit2:

Here's the code to get issue an access token in the desktop app (Ext JS):

    userLogin: function(){
        let deferred = new Ext.Deferred();
        let self = this;

        if (window.location.href.indexOf("#id_token") >= 0) {
            return self.processLoginResponse(deferred, self);
        }
        
        var settings = {
            authority: 'https://'   ONELOGIN_SUBDOMAIN   '.onelogin.com/oidc/2',    
            client_id: ONELOGIN_CLIENT_ID,
            redirect_uri: window.location.origin,
            response_type: 'id_token token',
            scope: 'openid profile',
        
            filterProtocolClaims: true,
            loadUserInfo: true
        };
        var mgr = new Oidc.UserManager(settings);

        mgr.signinRedirect({state:'some data'}).then(function() {
            deferred.resolve("signin redirect");
        }).catch(function(err) {
            deferred.reject(err);
        });

        return deferred.promise;
    },

    processLoginResponse: function(deferred, self){            
        var settings = {
            authority: 'https://'   ONELOGIN_SUBDOMAIN   '.onelogin.com/oidc/2',    
            client_id: ONELOGIN_CLIENT_ID,
            redirect_uri: window.location.origin,
            response_type: 'id_token token',
            scope: 'openid profile',
        
            filterProtocolClaims: true,
            loadUserInfo: true
        };

        var mgr = new Oidc.UserManager(settings);
        mgr.signinRedirectCallback().then(function(user) {
            Ext.Ajax.setDefaultHeaders({ 'Authorization': 'Bearer '   user.id_token });
            deferred.resolve("user signed in");
        }).catch(function(err) {
            console.log(err);
            deferred.reject(err);
        });

        return deferred.promise;
    }

CodePudding user response:

WEBSITE v API TECH STACKS

The tech stack you are using is designed for websites. When an HTML page is loaded, if there is no valid authentication cookie, the user is abruptly redirected to login.

Native clients instead call APIs via direct HTTP requests, and the above stack would attempt to redirect these requests, which would be the wrong behaviour.

Instead native apps should each implement their own OIDC flow to get an access token, using their own Client ID. The RFC8252 standard describes these flows. APIs should not need to know Client IDs of their callers.

API TECH STACK BEHAVIOUR

It is possible and usually recommended to use the same APIs for multiple related clients. If using C#, it is common for APIs to use the following option, where APIs validate JWT access tokens on every request:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = Config.Authority;
        options.Audience = Config.Audience;
    });

This is the Microsoft package you include in order to use it:

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.10" />

Typically the authority is the base URL of your authorization server and the audience is a value you define to represent one or more related APIs:

authority: https://login.example.com
audience: api.example.com

If required, APIs with the same audience can then forward the JWT to each other, so that they can all authorize requests in the same way. The Microsoft security stack will download and cache the token signing public key from the Authorization Server, then use it to digitally verify received access tokens and build a ClaimsPrincipal.

Once the JWT is validated you can also trust the claims it issued and authorize based on them, eg by defining authorization policies like this:

services.AddAuthorization(options =>
{
    options.AddPolicy("myPolicy", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(claim => 
                claim.Type == "myClaim" && claim.Value == "myValue
            )
        )
    );
});

One way to code authorization at the logic level is for your controllers to then apply an Authorize attribute in order to apply the policy:

[HttpGet("/api/resource")]
[Authorize(Policy = "myPolicy")]
public IActionResult GetResource()
{
    return Ok(new { data = "My data" });
}

JWT VALIDATION IN .NET FRAMEWORK

In the older .NET framework, the concepts are identical, with the only difference being that authorization attributes may work a little differently. Here is how a JWT is validated, where the Issuer / Authority still represents the base URL of the authorization server:

app.UseJwtBearerAuthentication(
    new JwtBearerAuthenticationOptions
    {
        TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidIssuer = Config.Issuer;
            ValidAudience = Config.Audience;
        }
    });
  • Related