OpenID Connect with .NET Core 2 & JBoss Keycloak

Michael Maier
10 min readNov 14, 2017

--

Building a .NET Core 2.0 based web based application where the user is authenticated using OpenID Connect through JBoss Keycloak authorization server didn’t feel like the fanciest job to do. As I didn’t have a solid knowledge (and I still don’t) about the standards it was a bit overwhelming at first. I wanted to share the details I found out while spending few evenings and days with the demo application.

My scenario consists of three services:

  • Keycloak OIDC Authorization Server (Keycloak)
  • Web application (WebApp) that is protected using OpenID Connect
  • Web API (WebAPI) protected with OAuth2 (bearer token)

The goal is to have protected pages in the WebApp which require the user to sign-in with OpenID through Keycloak server. In the WebApp we should be able to use the claims/roles given by the Keycloak and should be able to call the WebAPI with a bearer token. WebAPI should be able to know the identity of the calling user and not only the calling service in secure way.

High level architecture

Keycloak

Keycloak is an open source identity and access management solution which is commercially supported by JBoss. I was running Keycloak using the JBoss Keycloak official Docker image (https://hub.docker.com/r/jboss/keycloak/). By default there is no SSL enabled so I needed to run it behind HAProxy with SSL offload enabled. You can run it without SSL but some .NET classes refused to work without https prefix (but not sure if it’s required for this exact scenario). If you run Keycloak behind a proxy you will need to enable the proxy forwarding as described in the docker page to make it work properly. In addition you need to supply the admin username and password as variables to the docker container to be able to login.

If you were lucky enough you should now be able to see the login prompt and jump right in when browsing to https://auth.mydomain.com/auth/admin. Then in the Keycloak admin console we need to create and configure the clients which in this case are WebApp and WebAPI. Configuration for WebApp:

  • Client Protocol = openid-connect
  • Access Type = confidential (there is a shared secret which is not revealed to to the user/browser)
  • Standard Flow Enabled = on
  • Implicit Flow Enabled = on
  • Credentials / Client Authenticator = Client id and Secret
  • Configure the proper URL’s like Root URL and redirect URIs

We need to enable standard and implicit flows as we are going to use the “code id_token” hybrid flow in this example. The flow is well explained in the following article: https://medium.com/@robert.broeckelmann/saml2-vs-jwt-understanding-openid-connect-part-2-f361ca867baa. In summary when the user authenticates by entering the credentials Keycloak will give a code and an id-token for the WebApp. This prevents the user/browser from actually seeing the access token which is retrieved by the WebApp directly using the code in combination with the secret key that identifies the confidential client (WebApp).

Then we also need to create a client configuration for the WebAPI:

  • Client Protocol = openid-connect
  • Access Type = confidential (again a trusted client)
  • Standard Flow Enabled = off (the API is only called with a bearer token)
  • Implicit Flow Enabled = off (the API is only called with a bearer token)
  • Service Accounts Enabled = on (the API might want to call other services)
  • Authorization = on
  • Credentials / Client Authenticator = Client id and Secret
  • Configure the proper Root URL

We also need a new user to Keycloak but that is easy to create through the admin console. Just remember to set the user active.

WebApp

  • Install IdentityModel Nuget (OpenID Connect & OAuth 2.0 client library)
  • Configure cookie and OpenIDConnect in startup.cs and enable authentication
  • Add [Authorize] attribute to the controller methods you want to protect. I wanted to protected the About-method in the ASP.NET auto generated controller example.

WebAPI

In the WebAPI we only configure the JSON Web Token support as the bearer token (= access token generated by Keycloak) should be the only way to call the service. Here we actually have an issue that I have been unable to solve (assuming it can be solved in a better way) since the Audience validation must be disabled. In this case the audience in the access token is actually the WebApp for which the token was originally generated for. However that might not be a show stopper as we are able to check the validity of the token for this service in a different way explained later on.

We also need to enable the authentication via app.UseAuthentication is the configure method like in the WebApp. This must be done before app.UseMvc line. Now in the controller you can apply the [Authorize] attribute to the protected method. But lets forget the configuration now and see how it actually works!

OICD Flow under the hood

When starting the WebApp you probably arrive to the default ASP.NET demo landing page. When you click About (assuming you have put the [Authorize] attribute to that method) you will be redirected to the Keycloak login page. That happens because you have configured the OpenID Authority URL (https://auth.myhost.com/auth/realms/master in the example) and told ASP.NET to require authentication. However there is more! Before the user is redirected to Keycloak some sneaky GET requests will happen in he background:

GET https://auth.mydomain.com/auth/realms/master/.well-known/openid-configuration HTTP/1.1  
Connection: Keep-Alive
Accept-Encoding: peerdist
User-Agent: Microsoft ASP.NET Core OpenIdConnect handler
x-ms-request-root-id: cad85c6e-4c05937ca90cd3e7
x-ms-request-id: |cad85c6e-4c05937ca90cd3e7.1.
Request-Id: |cad85c6e-4c05937ca90cd3e7.1.
X-P2P-PeerDist: Version=1.0
Host: auth.mydomain.com

HTTP/1.1 200 OK
Cache-Control: no-cache, must-revalidate, no-transform, no-store
Content-Type: application/json
Content-Length: 1857
Date: Sun, 12 Nov 2017 20:40:48 GMT

{"issuer":"https://auth.mydomain.com/auth/realms/master","authorization_endpoint":"https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/auth","token_endpoint":"https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/token","token_introspection_endpoint":"https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/token/introspect","userinfo_endpoint":"https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/userinfo","end_session_endpoint":"https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/logout","jwks_uri":"https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/certs","check_session_iframe":"https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials"],"response_types_supported":["code","none","id_token","token","id_token token","code id_token","code token","code id_token token"]
... and so on

What actually happened is that ASP.NET OpenID Connect library is fetching the meta data from the known address. From the metadata it will extract the endpoints for certificate and user information. This information is then stored and I assume it is in the cookie but I might be wrong. Before the actual user redirection the ASP.NET will still go and fetch the certificate data:

GET https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/certs HTTP/1.1  
Connection: Keep-Alive
Accept-Encoding: peerdist
User-Agent: Microsoft ASP.NET Core OpenIdConnect handler
x-ms-request-root-id: cad85c6e-4c05937ca90cd3e7
x-ms-request-id: |cad85c6e-4c05937ca90cd3e7.2.
Request-Id: |cad85c6e-4c05937ca90cd3e7.2.
X-P2P-PeerDist: Version=1.0
Host: auth.mydomain.com

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 462
Date: Sun, 12 Nov 2017 20:40:49 GMT

{"keys":[{"kid":"kSJ9Gn-EGGW6yzlskM3KGFYns058RXYb7BTuAcPQwhI","kty":"RSA","alg":"RS256","use":"sig","n":"hfJ9HOPGeG6iahgcRx6ptXWnZSWcRRwKrzva34g_hwLoF-DBDIYN6nP6xRNmwxns2KiLtyhQKXGtlK6ZvjLGYTqYbIZPFQp2OMcDs5x_n1ufNdYiJV6snCXQ1sKwwA220LqDMCVwWFGdEI1pJOaSzxxNIfdpYheqEOV8Na_byFo3YzY9oTGhpsqGoS0pZHt9HRSrRExaO5W-fleBO7Efw4Nc0gChgn2dGnr69OQycsKdha6sGup1DFk089dZmJH3C6LMwjaHzuzSXgpKh3tr29B4cWF8KrD4p23F0g8J_GRW_1zex0ZGXtiJnaqQ-xgVZdVmsF84XCBzwXIiUJArUw","e":"AQAB"}]}

And yes I was thinking the same. It doesn't look like a public key you could use to check the signature of the token (which you don't even have yet)! But it actually is the key in modulus & exponent format and you can always check the nitty gritty details from the RFC (https://tools.ietf.org/html/rfc7517). We are using RSA key pairs as that's the default configuration for Keycloak (RS256 is the algorithm) but the JSON Web Token standard also supports other means like HMAC (read more about the options from https://connect2id.com/products/nimbus-jose-jwt/examples/jwk-generation)

But now back to the user flow. When the user correctly enters the credentials the Keycloak server will use the HTTP POST to send the user back to the WebApp. Here is what the WebApp will get:

POST http://localhost:56850/signin-oidc HTTP/1.1  
Accept: text/html, application/xhtml+xml, */*
Accept-Language: fi-FI
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Content-Length: 2010
Host: localhost:56850
Pragma: no-cache
Cookie: .AspNetCore.OpenIdConnect.Nonce.CfDJ8NOPq9rA ...shortened... NJstvFTmCk=N
&code=eyJhbGciOiJkaXIi ...shortened... PtB13vlzQ
&not-before-policy=0
&id_token=eyJhbGciOiJSUzI...shortened...rYQSvrW2E

We can clearly see that the WebApp is receiving the code and id-token as it should. So what's inside of those cryptic codes?? Well the code is just a code but the token has some content. There is a nice service https://jwt.io/ that you can use to check the token content, signature and you can even create your own tokens, magic! And here is the content of the id_token:

{
"jti": "b208bf55-4d41-43fa-aafe-6d455f87cc41",
"exp": 1510519652,
"nbf": 0,
"iat": 1510519352,
"iss": "https://auth.mydomain.com/auth/realms/master",
"aud": "WebApp",
"sub": "8f75e7fc-bb25-4365-b29b-6d2180017a7b",
"typ": "ID",
"azp": "WebApp",
"nonce": "636461157341778008.MGM2NGExMWMtY2E5Ni00OTFhLTgzNzItMzczNjFkZGZhNzM2M2JhOTlmZTMtMzg3Ny00ZGQ5LWJkZDItMDFiOWZkNTY3ZmI5",
"auth_time": 1510519352,
"session_state": "445d222b-de3a-42c7-b5f1-687f1426ea31",
"c_hash": "ERw6HvjwPbfzo5fdlp1LuQ",
"acr": "1",
"name": "Michael Maier",
"preferred_username": "eksek",
"given_name": "Michael",
"family_name": "Maier",
"email": "michael.maier@mydomain.com"
}

There is also the header section containing the algorithm and token type but it's not displayed here. So now we know who the user is and the ASP.NET OpenID Connect library will automatically check the signature of the token as well as other validity parameters depending how you have configured it. Then off we go to fetch the access token. This is also done automatically so you don't need to care about it. The claims will be automatically found from the User object in ASP.NET but you can also fetch the tokens manually in the controller if and when you need them (e.g. you want to call the WebAPI).

POST https://auth.mydomain.com/auth/realms/master/protocol/openid-connect/token HTTP/1.1  
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
User-Agent: Microsoft ASP.NET Core OpenIdConnect handler
x-ms-request-root-id: cad85c6f-4c05937ca90cd3e7
x-ms-request-id: |cad85c6f-4c05937ca90cd3e7.1.
Request-Id: |cad85c6f-4c05937ca90cd3e7.1.
Content-Length: 513
Host: auth.mydomain.com

client_id=WebApp&client_secret=d057917b-8eec-401d-b72d-ac4e6d76c264&code=eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..1h1vq6mxp6FWqF4PflXZLg.31esKQVBKAzHFZ8kNGKLV15P1GDvVVIKQ2v6n6upzrRxhnnKn1feTUyhgnb_ePBwchwua0yWC8xbT8BjHQ5Qn1nFcuOtjeHcem4YF1Mtbd9E7LRPPazNsLcPUmQb-rQ22NXy63fTx9y5bwCciGr6ScpKD2AG8D5tmqQAeMABSpEb1E6LCRYSv8kD98279yLFkbzju-T4IseQDxREFFs6DkEEDlEZ_Ll6QKeoZYbqyY9BVRT2nwstgaoemkawohwc.SarMZ9Fm5EwvRPtB13vlzQ&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A56850%2Fsignin-oidc

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 3886
Date: Sun, 12 Nov 2017 20:42:32 GMT

{"access_token":"eyJhbGciOiJSUzI1N...shortened...xTdzbFYwBQ","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbG...shortened...LchkT2n4wA","token_type":"bearer","id_token":"eyJhbGc...shortened...IeP8Mw","not-before-policy":0,"session_state":"445d222b-de3a-42c7-b5f1-687f1426ea31","scope":""}

As you can see the tokens were fetched using the client's identity (WebApp) authenticated with the secret key. WebApp received access_token, id_token and a refresh_token. Refresh token can be used to get a new access_token since the access_token should always be a short lived one (default in Keycloak was one minute). Inside the access_token we have some new data like resource_access (which I haven't talked about yet). The refresh token has less fields but the content is rather similar without the user details.

{
"jti": "01a7b3fb-ea33-4495-8bca-0ccc6cfe5bf1",
"exp": 1510519652,
"nbf": 0,
"iat": 1510519352,
"iss": "https://auth.mydomain.com/auth/realms/master",
"aud": "WebApp",
"sub": "8f75e7fc-bb25-4365-b29b-6d2180017a7b",
"typ": "Bearer",
"azp": "WebApp",
"nonce": "636461157341778008.MGM2NGExMWMtY2E5Ni00OTFhLTgzNzItMzczNjFkZGZhNzM2M2JhOTlmZTMtMzg3Ny00ZGQ5LWJkZDItMDFiOWZkNTY3ZmI5",
"auth_time": 1510519352,
"session_state": "445d222b-de3a-42c7-b5f1-687f1426ea31",
"acr": "1",
"allowed-origins": [
"http://localhost:56850"
],
"resource_access": {
"WebAPI": {
"roles": [
"apiuser"
]
}
},
"name": "Michael Maier",
"preferred_username": "eksek",
"given_name": "Michael",
"family_name": "Maier",
"email": "michael.maier@mydomain.com"
}

Now in the protected controller we have the ASP.NET User object filled with the user details. We can also obtain the the raw tokens easily:

string accToken = HttpContext.GetTokenAsync(“access_token”).Result;
string idToken = HttpContext.GetTokenAsync(“id_token”).Result

Calling the protected WebAPI

Calling the WebAPI from the WebApp controller is actually pretty easy:

var client = new HttpClient();client.SetBearerToken(accessToken);var content = await client.GetStringAsync("http://localhost:52482/api/values");

The SetBearerToken helper method is something you get when installing the IdentityModel nuget.

The WebAPI will call the Keycloak server in the background when receiving the API call to retrieve the meta data and certificate details like the WebApp did before the redirecting the user to the login page of Keycloak. So the validation of the token is done automatically and you don’t need to care about that. However you can’t validate the Audience as that’s “wrong” as it is the WebApp and not the WebAPI.

To protect the WebAPI it should check that the user actually has a permission / role to call this API. That’s is achieved by checking that we have the required resource_access properties in the access_token. We know that the access_token is not generated for this WebAPI but we do know that it’s generated by a trusted Keycloak instance which claims that the user has specific roles which probably is enough to our use case. Keycloak also has a thing called token introspection endpoint where you can actually use Keycloak to validate the access_token. The introspection endpoint uses basic validation and the client id and secret key are used a credentials while the access_token is given as a parameter.

Setting up the resource access

The settings and how the access management works in Keycloak might seem a bit overwhelming. I’m no expert in that so I just did the minimal configuration to get the stuff working.

Navigate to the WebAPI settings and click the Roles tab. Create two new roles apiuser and apireader. The naming is bad but it just happened, sorry.

Next you need to navigate to the WebApp settings as you need to create the appropriate Scope Mappings for it. As the access_token is obtained for the WebApp it also needs to have the rights to the roles! In addition the user must have rights to the roles so the resources_access defined in the access_token is the largest common subset of the roles allowed for both the user and the client. To do the Scope Mappings you need to use the Client Roles dropdown and select WebAPI as that’s the client holding the roles and then assign those roles to the WebApp.

Next go to the user settings and again navigate to the Role Mappings. Do the same role assignment but just assign the apiuser role for this user. That’s the reason why in the access_token we only see the apiuser role for the WebAPI for the user/WebApp combination:

"resource_access": {
"WebAPI": {
"roles": [
"apiuser"
]
}

Closing words

Now we have the basic setup running jeiiii! As I said the permission management in Keycloak is rather complex and so is the OpenID Connect / OAuth2 standard as well. There are always more than one way of doing things and you should investigate them and pick the appropriate one.

For me there are still some uninvestigated details:

  • What’s is actually stored in the cookies and in which format?
  • How should we use the token caches or is it used automatically?
  • How does the token renewal actually work in the ASP.NET Core 2?
  • How do scopes, roles, permissions and resources map together?

Many things that are done automatically by the OpenID Connect library can be done manually if needed. However if you set the Authority property in the options it will overrule some manual configurations (e.g. IssuerSigningKeys). If you do want to get the signing keys manually do not set the Authority and use the meta data discovery to fetch the keys for you:

IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>("https://auth.mydomain.com/auth/realms/master/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());OpenIdConnectConfiguration openIdConfig = configurationManager.GetConfigurationAsync(CancellationToken.None).Result;

After that you can set the IssuerSigningKeys in the TokenValidationParameters to openIdConfig.SigningKeys. In a case where the WebAPI wouldn’t be able to contact the Keycloak you could also generate the SigningKeys from the public key string.

--

--