Friday, August 12, 2022

OAuth Integration - Command-Line Validation

What seemed like a simple endeavor -- learn how to implement OAuth to protect web services and portal applications using Java and Spring -- turned out to be much more complicated than expected. The posts in this series are targeting two groups -- developers with little background in the goals and mechanics of Oauth and more experienced developers who just need help avoiding pitfalls resulting from inconsistencies in implementations of OAuth. The content is divided into multiple posts:

OAuth Integration - Overview
OAuth Integration - Challenges
OAuth Integration - Authorization Server
OAuth Integration - Command Line Validation<---- YOU ARE HERE
OAuth Integration - Resource Server
OAuth Integration - Service Client
OAuth Integration - User Portal Application


The prior installment in this series provided steps on installing Keycloak for use as an Authorization Server then configuring users and client applications that can be used in subsequent development. Before attempting design work at the Java layer, it is VERY helpful to gain familiarity with the request and response formats of key transactions. Doing so is useful for all of the following reasons:

  • Doing so ensures correct endpoints have been provided for use in application code configuration
  • Doing so confirms client-id and client-secret values provided are correct and accepted by the target Authorization Server
  • For local test Keycloak instances, doing so ensures it starts correctly and is reachable from where test users and applications will run.
  • Doing so allows raw request / response payloads to be viewed to better understand how NON-standard OAuth is and how much normalization will still be required of developers.

That last point cannot be emphasized enough. If you need to integrate to multiple vendors but start all work with a single Authorization Server implementation, the resulting code will likely NOT reflect all of the accomodations required to overcome the lack of consistency across vendors.


The testing described here can be performed from the command line via tools like curl or via graphical tools such as Postman. Writing about such testing is easier when referencing typed commands than graphical user interfaces so the examples here will always use curl. However, enough information will be provided to allow Postman users to easily accomplish the same tests.

Keycloak altered its URI scheme in version 17.x eliminating /auth/ within all URIs. Older online references will have strings of the form http://host:port/auth/realms/mdhlabs while the newer versions only use http://host:port/realms/mdhlabs.


Obtaining Endpoints Needed for Client Integration

After a client application is configured within the Authorization Server, the developer / administrator of the application must obtain a variety of URL endpoints operating on the Authorization Server to use for integration. The means to obtain the endpoints depends on the type of Authorization Server. For implementations providing OpenID based access, a specific endpoint called the "well-known" URI will return a JSON payload listing all endpoints supported by the instance. Other providers which are not OpenID compliant may just publish documentation listing their endpoints or expect the administrator of the Authorizaton Server to provide the information to the application developer. The Keycloak platform implements OpenID so it supports both methods of access.

Here are the well-known URIs of the providers that will be used in the examples to come:

Provider OpenID Discovery URL
Facebookhttps://www.facebook.com/.well-known/openid-configuration
GitHubhttps://token.actions.githubusercontent.com/.well-known/openid-configuration
Googlehttps://accounts.google.com/.well-known/openid-configure
Keycloakhttp://host.domain.com:port/realms/mdhlabs/.well-known/openid-configure
LinkedIn(not supported)

Keycloak allows endpoint data to be extracted via the well-known URI above but also provides a button within the administrator console on the General tab of the Realm page that will display the information.

If the admin user view is used, a pop-up window will be displayed with the content. If fetched from the URI, a large JSON encoded response similar to the following will be returned.

{"issuer":"http://192.168.99.10:8011/realms/mdhlabs","authorization_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/auth","token_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token","introspection_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token/introspect","userinfo_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/userinfo","end_session_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/certs","check_session_iframe":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials","urn:ietf:params:oauth:grant-type:device_code","urn:openid:params:grant-type:ciba"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"id_token_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","email","microprofile-jwt","phone","acr","web-origins","roles","address","profile","offline_access"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","ES384","RS384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token","revocation_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/revoke","introspection_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token/introspect","device_authorization_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/auth/device","registration_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/clients-registrations/openid-connect","userinfo_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/ext/ciba/auth"}}

The jwks_uri parameter returned in the response merits discussion. That is a good example of the degree of non-standardization in the implementation of OAuth2 functionality and the complexity of coding around those failed standards. In an ideal, standards-driven ecosystem, a component configured to use OAuth2 will perform the following steps at startup:

  1. read configuration parameters from application.properties providing the issuer-uri of the Authorization Server to be used
  2. synthesize a longer URL of the form $issuer_uri/.well-known/openid-configuration and perform a GET to that endpoint to fetch this list of other configuration endpoints
  3. extract the jwks_url parameter from that list and perform a GET to that endpoint to fetch the public encryption keys that can be used to decrypt the security hash of any tokens generated by the Authorization Server that are returned to a client then submitted to the Resource Server relying on that token

In the case of Keycloak, it does expose its OpenID configuration endpoints at the expected endpoint. However, the response sent by Keycloak on that endpoint doesn't use the standard name of jwks_url for the encryption key information, it uses jwks_uri ("eye" instead of "el"). That means libraries built to step through this chain cannot use automatic configuration and instead require the jwks_url value to be supplied via application.properties or configuration within the Java source that configures OAUth2 Resource Server functionality. And adding MORE CONFUSION to the mix, instead of using spring.security.oauth2.resourceserver.jwt.jwks_url as the parameter name, the Spring Security OAuth2 library actually requires spring.security.oauth2.resourceserver.jwt.jwk-set-uri as the parameter name.

So much for standards.

One DRAWBACK to automated configuration via discovery using /.well-known/openid-configuration is that if the OAuth2 Authorization Server is unavailable at the time a component is started, the component will fail to start since it cannot fetch the rest of the AS endpoints to complete internal configurations. If instead the jwk-set-uri parameter is provided via application.properties or explicit configuration, the component will at least start but will fail later when it actually attempts to fetch the keys the first time.

Authorization Code Requests

Interactive authorization of a user in an Authorization Server is initiated by redirecting the user to the authorization-uri endpoint of the Authorization Server. This doesn't immediately authenticate the user and return an authorization code. It instead triggers the Authorization Server to generate its login page which renders a login form with a submit action to another URI within the Authorization Server that performs the authentication THEN returns a response to the user which redirects their browser to the redirect-uri initially provided. This flow keeps the human's (username + password) out of the hands of the client app -- it is only seeing a third party token that gives it permission to convert that permission into an access token.

This indirect flow makes it difficult to execute the authorization flow purely with curl / wget commands. However, it is possible to see the login form generated by the Authorization server and also gain some understanding of validations the Authorization Server performs on incoming requests by testing from the command line. Here are the parameters of the request for our mdhlabs realm in Keycloak.

Request URLhttp://192.168.99.10:8011/realms/mdhlabs/openid-connect/auth
MethodPOST
Body
client_id=DependsGUI&
client_secret=V3wpwsY31P7PcmUHzNVNWmWK6016q4FU&
response_type=code&
scope=openid email&
redirect_uri=https://192.168.99.10:8022/depends/gui/access/oauth2/callback/keycloak

The output from entering this command

curl --verbose -d "client_id=DependsGUI" -d "response_type=code" -d "state=randomstring" -d "redirect_uri=https://fedora1.mdhlabs.com:8022/depends/gui/access/oauth2/callback/keycloak" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/auth"


is provided below.

mdh@fedora1:~/gitwork/oauthdependsresource $ curl --verbose -d "client_id=DependsGUI" -d "response_type=code" -d "state=randomstring" -d "redirect_uri=https://fedora1.mdhlabs.com:8022/depends/gui/access/oauth2/callback/keycloak" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/auth" * Trying 192.168.99.10:8011... * Connected to 192.168.99.10 (192.168.99.10) port 8011 (#0) > POST /realms/mdhlabs/protocol/openid-connect/auth HTTP/1.1 > Host: 192.168.99.10:8011 > User-Agent: curl/7.82.0 > Accept: */* > Content-Length: 148 > Content-Type: application/x-www-form-urlencoded > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Referrer-Policy: no-referrer < X-Frame-Options: SAMEORIGIN < Strict-Transport-Security: max-age=31536000; includeSubDomains < X-Robots-Tag: none < Cache-Control: no-store, must-revalidate, max-age=0 < X-Content-Type-Options: nosniff < Content-Security-Policy: frame-src 'self'; frame-ancestors 'self'; object-src 'none'; < Set-Cookie: AUTH_SESSION_ID=5b8643e7-cc61-454d-8888-8cfaba87fbe8; Version=1; Path=/realms/mdhlabs/; SameSite=None; Secure; HttpOnly < Set-Cookie: AUTH_SESSION_ID_LEGACY=5b8643e7-cc61-454d-8888-8cfaba87fbe8; Version=1; Path=/realms/mdhlabs/; HttpOnly < Set-Cookie: KC_RESTART=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2E4NGYzOC02ZDAyLTRhZDUtOTZmYS05Nzg5OGE4OGQxZDYifQ.eyJjaWQiOiJEZXBlbmRzR1VJIiwicHR5Ijoib3BlbmlkLWNvbm5lY3QiLCJydXJpIjoiaHR0cHM6Ly9mZWRvcmExLm1kaGxhYnMuY29tOjgwMjIvZGVwZW5kcy9ndWkvYWNjZXNzL29hdXRoMi9jYWxsYmFjay9rZXljbG9hayIsImFjdCI6IkFVVEhFTlRJQ0FURSIsIm5vdGVzIjp7ImlzcyI6Imh0dHA6Ly8xOTIuMTY4Ljk5LjEwOjgwMTEvcmVhbG1zL21kaGxhYnMiLCJyZXNwb25zZV90eXBlIjoiY29kZSIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vZmVkb3JhMS5tZGhsYWJzLmNvbTo4MDIyL2RlcGVuZHMvZ3VpL2FjY2Vzcy9vYXV0aDIvY2FsbGJhY2sva2V5Y2xvYWsiLCJzdGF0ZSI6InJhbmRvbXN0cmluZyJ9fQ.u4oQdQywesN0P7zz4Ktmta0kxBpikl3fHSjEtC_Q47c; Version=1; Path=/realms/mdhlabs/; HttpOnly < X-XSS-Protection: 1; mode=block < Content-Language: en < Content-Type: text/html;charset=utf-8 < content-length: 3406 < <!DOCTYPE html> <html class="login-pf"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="robots" content="noindex, nofollow"> <meta name="viewport" content="width=device-width,initial-scale=1"/> <title>Sign in to mdhlabs</title> <link rel="icon" href="/resources/hn7nr/login/keycloak/img/favicon.ico" /> <link href="/resources/hn7nr/common/keycloak/web_modules/@patternfly/react-core/dist/styles/base.css" rel="stylesheet" /> <link href="/resources/hn7nr/common/keycloak/web_modules/@patternfly/react-core/dist/styles/app.css" rel="stylesheet" /> <link href="/resources/hn7nr/common/keycloak/node_modules/patternfly/dist/css/patternfly.min.css" rel="stylesheet" /> <link href="/resources/hn7nr/common/keycloak/node_modules/patternfly/dist/css/patternfly-additions.min.css" rel="stylesheet" /> <link href="/resources/hn7nr/common/keycloak/lib/pficon/pficon.css" rel="stylesheet" /> <link href="/resources/hn7nr/login/keycloak/css/login.css" rel="stylesheet" /> </head> <body class=""> <div class="login-pf-page"> <div id="kc-header" class="login-pf-page-header"> <div id="kc-header-wrapper" class="">mdhlabs</div> </div> <div class="card-pf"> <header class="login-pf-header"> <h1 id="kc-page-title"> Sign in to your account </h1> </header> <div id="kc-content"> <div id="kc-content-wrapper"> <div id="kc-form"> <div id="kc-form-wrapper"> <form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="http://192.168.99.10:8011/realms/mdhlabs/login-actions/authenticate?session_code=8kAnKiVlsCn1754ziOwIF_qQs5uLWKCl3-yKdde0mlI&amp;execution=e690be53-bc78-42ba-ae0d-6e37fe86ad26&amp;client_id=DependsGUI&amp;tab_id=iZoZJyEDAB0" method="post"> <div class="form-group"> <label for="username" class="pf-c-form__label pf-c-form__label-text">Username or email</label> <input tabindex="1" id="username" class="pf-c-form-control" name="username" value="" type="text" autofocus autocomplete="off" aria-invalid="" /> </div> <div class="form-group"> <label for="password" class="pf-c-form__label pf-c-form__label-text">Password</label> <input tabindex="2" id="password" class="pf-c-form-control" name="password" type="password" autocomplete="off" aria-invalid="" /> </div> <div class="form-group login-pf-settings"> <div id="kc-form-options"> </div> <div class=""> </div> </div> <div id="kc-form-buttons" class="form-group"> <input type="hidden" id="id-hidden-input" name="credentialId" /> <input tabindex="4" class="pf-c-button pf-m-primary pf-m-block btn-lg" name="login" id="kc-login" type="submit" value="Sign In"/> </div> </form> </div> </div> </div> </div> </div> </div> </body> </html> * Connection #0 to host 192.168.99.10 left intact mdh@fedora1:~/gitwork/oauthdependsresource $

What are these request parameters doing? The client_id parameters names the client application built in the Authorization Server. The response_type parameter specifies we want an authorization code returned if authentication succeeds so we can convert it to an access token. The state parameter allows the app to synthesize a random string and pass it to the Authorization Server which will return it to us. The intent is for this value to act as a nonce to allow the app to ensure a hacker has not crafted a script to bounce old requests through the flow in an attempt to break into the application. The redirect_uri paramter instructs the Authorization Server to send a redirect to the human browser to that URI within the application so the second leg of the process can execute to convert the authorization code into an access token. The format of the redirect_uri is not random. The last parameter identifies the Authorization Server returning the response so an application integrated to more than one knows which response type it is handling.

It's useful to try some BAD commands with invalid parameters or missing parameters to understand how the Authorization Server will respond. In particluar, most AS implementations impose validation checks on the redirect_uri. If configured to expect https://fedora1.mdhlabs.com:8443:/depends/gui/access/oauth2/callback/keycloak but some request arrives specifying https://fedora1.hackerdomain.com:8443:/depends/gui/access/oauth2/callback/keycloak, the AS will reject the request as a security risk. (Strangely, Keycloak does not. It will render the login page and blindly forward the user back to the specified URI.)

curl --verbose -d "client_id=DependsGUI" -d "response_type=code" -d "state=randomstring" -d "redirect_uri=https://fedora1.mdhlabs.com:8022/depends/gui/access/oauth2/callback/keycloak" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/auth"


Access Token Requests (grant_type=authorization_code)

Access tokens are generated by invoking the token_uri endpoint of the Authorization Server. When the client application and authorization server are not part of the same administrative / security domain, access tokens will use grant_type=authorization_code flow which means the authorization code was obtained separately by the end user interacting directly with the authorization server, having an authorization code returned to the user who then submits that to the client application. This flow keeps the human's (username + password) out of the hands of the client app -- it is only seeing a third party token that gives it permission to convert that permission into an access token.

The structure of the required request format is summarized below.

Request URLhttp://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token
MethodPOST
Body
client_id=DependsGUI&
client_secret=V3wpwsY31P7PcmUHzNVNWmWK6016q4FU
grant_type=authorization_code&
redirect_uri=http://192.168.99.10:8022/depends/gui/access/oauth2/callback/keycloak&
code=longrandomcodestring

An example of invoking this endpoint using curl is provided below.

curl --verbose -d "client_id=DependsGUI" -d "grant_type=authorization_code" -d "client_secret=V3wpwsY31P7PcmUHzNVNWmWK6016q4FU" -d "redirect_uri=https://fedora1.mdhlabs.com:8022/depends/gui/access/oauth2/callback/keycloak" -d "code=b7b78b97-ce08-4908-9caa-65bdfe76bec4.99b5fc3c-bfef-4518-9aa4-4fafb143eb7a.a416b0a7-c05c-48c1-91f0-87cf80bb0661" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token"

root@fedora1:/opt/keycloak1800 # curl --verbose -d "client_id=DependsGUI" -d "grant_type=authorization_code" -d "client_secret=V3wpwsY31P7PcmUHzNVNWmWK6016q4FU" -d "redirect_uri=http://192.168.99.10:8022/depends/gui/access/oauth2/callback/keycloak" -d "code=b7b78b97-ce08-4908-9caa-65bdfe76bec4.99b5fc3c-bfef-4518-9aa4-4fafb143eb7a.a416b0a7-c05c-48c1-91f0-87cf80bb0661" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token" * Trying 192.168.99.10:8011... * Connected to 192.168.99.10 (192.168.99.10) port 8011 (#0) > POST /realms/mdhlabs/protocol/openid-connect/token HTTP/1.1 > Host: 192.168.99.10:8011 > User-Agent: curl/7.71.1 > Accept: */* > Content-Length: 296 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 296 out of 296 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Referrer-Policy: no-referrer < X-Frame-Options: SAMEORIGIN < Strict-Transport-Security: max-age=31536000; includeSubDomains < Cache-Control: no-store < X-Content-Type-Options: nosniff < Pragma: no-cache < X-XSS-Protection: 1; mode=block < Content-Type: application/json < content-length: 2334 < {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NTcwNDk0ODQsImlhdCI6MTY1NzA0OTEyNCwiYXV0aF90aW1lIjoxNjU3MDQ5MTI0LCJqdGkiOiJmMDMzN2UxZC02NWRhLTRhYWEtYjY2OS1jODIxYzRmYzM1MDkiLCJpc3MiOiJodHRwOi8vMTkyLjE2OC45OS4xMDo4MDExL3JlYWxtcy9tZGhsYWJzIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjZmZDI3Mjg4LWVmNWEtNDRmNi05YmZjLTIzYWQzMjRmMjA5ZCIsInR5cCI6IkJlYXJlciIsImF6cCI6IkRlcGVuZHNHVUkiLCJzZXNzaW9uX3N0YXRlIjoiMjQ2NzMyZjgtOGJkOC00MWVlLWE2MDUtYmM3ZDE5ZTNmZTI4IiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly8xOTIuMTY4Ljk5LjEwOjgwMjIiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwibWRobGFic2dldG9ubHkiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtbWRobGFicyJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiIyNDY3MzJmOC04YmQ4LTQxZWUtYTYwNS1iYzdkMTllM2ZlMjgiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJtZGhsYWJzZ2V0b25seSIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIl0sIm5hbWUiOiJFcmljIENsYXB0b24iLCJncm91cHMiOlsib2ZmbGluZV9hY2Nlc3MiLCJtZGhsYWJzZ2V0b25seSIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIl0sInByZWZlcnJlZF91c2VybmFtZSI6ImVyaWNjbGFwdG9uIiwiZ2l2ZW5fbmFtZSI6IkVyaWMiLCJmYW1pbHlfbmFtZSI6IkNsYXB0b24iLCJlbWFpbCI6ImVyaWMuY2xhcHRvbkByc29yZWNvcmRzLmNvbSJ9.RJHJMnZ3roSjPyxybb1ZLMiSxRtJA6EzOBKAAViHsOx4x__iFuKC4ZRcrhYLOZXMGJfIxhSuWvxsPWagZDGUHYLnP7Qr_s5tYqXvP3lM0fj-7Uxk6dJMiWefCMJ2gtlsD_DsGnF0YO37BeZiCi3IayQ9cpBN9NL29Gxk99C_DG4inlxxdSvCY7iffDEFdP4wYjsEB_94wLxCPWbEiDwUGolw3hSoY454K5yzzHfBaghtqKHcpasooaqA0h3mH8yt3lrhKXaJeFaAQIZqAekWoI35jQSKmThqD9W8tieatbNs9yoAtJKhjBRmYr-6Lk0E0haIdq7PNnHRN-3hTQlfRg","expires_in":360,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2E4NGYzOC02ZDAyLTRhZDUtOTZmYS05Nzg5OGE4OGQxZDYifQ.eyJleHAiOjE2NTcwNTA5MjQsImlhdCI6MTY1NzA0OTEyNCwianRpIjoiMTc2NzE0YjYtNWRlMS00NmZlLWFhMTMtZGFiZDEzNjFhZjU1IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6Imh0dHA6Ly8xOTIuMTY4Ljk5LjEwOjgwMTEvcmVhbG1zL21kaGxhYnMiLCJzdWIiOiI2ZmQyNzI4OC1lZjVhLTQ0ZjYtOWJmYy0yM2FkMzI0ZjIwOWQiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiRGVwZW5kc0dVSSIsInNlc3Npb25fc3RhdGUiOiIyNDY3MzJmOC04YmQ4LTQxZWUtYTYwNS1iYzdkMTllM2ZlMjgiLCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiMjQ2NzMyZjgtOGJkOC00MWVlLWE2MDUtYmM3ZDE5ZTNmZTI4In0.-QwrKQQqCWGJstd_QrfOKndBsi072KnL6AGe7OqpWRw","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NTcwNDk0ODQsImlhdCI6MTY1NzA0OTEyNCwiYXV0aF90aW1lIjoxNjU3MDQ5MTI0LCJqdGkiOiI4ZGUwOTVjZS04M2Y1LTQyMmYtODlmNi0xM2NmODMwZWY3MTIiLCJpc3MiOiJodHRwOi8vMTkyLjE2OC45OS4xMDo4MDExL3JlYWxtcy9tZGhsYWJzIiwiYXVkIjoiRGVwZW5kc0dVSSIsInN1YiI6IjZmZDI3Mjg4LWVmNWEtNDRmNi05YmZjLTIzYWQzMjRmMjA5ZCIsInR5cCI6IklEIiwiYXpwIjoiRGVwZW5kc0dVSSIsInNlc3Npb25fc3RhdGUiOiIyNDY3MzJmOC04YmQ4LTQxZWUtYTYwNS1iYzdkMTllM2ZlMjgiLCJhdF9oYXNoIjoiTTZqNzhBU3dJN0NBMlVOVkUxMUUzQSIsInNpZCI6IjI0NjczMmY4LThiZDgtNDFlZS1hNjA1LWJjN2QxOWUzZmUyOCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiRXJpYyBDbGFwdG9uIiwiZ3JvdXBzIjpbIm9mZmxpbmVfYWNjZXNzIiwibWRobGFic2dldG9ubHkiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtbWRobGFicyJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJlcmljY2xhcHRvbiIsImdpdmVuX25hbWUiOiJFcmljIiwiZmFtaWx5X25hbWUiOiJDbGFwdG9uIiwiZW1haWwiOiJlcmljLmNsYXB0b25AcnNvcmVjb3Jkcy5jb20ifQ.RBThZG8MG6FmIh65qiH9TwBySHJcV_EjKT4xANU86J0nFhbkG_TKf4SLsc1A6aNSQCHF7UMV1ILB9TmoGHCXpsrctjQP3fVeuyAAonfIzFhokoqk5rJlWm8Eriscap0CHOWl42FInDI2Vu1oO1-nNT6nyhdl8VHmGmC4rtIBXZGDooKdmxP3jfCffj6TKEGOjjl2UmEN8JqM29Ig0VNL6z361by-5BCTXa3prvb7NXulQAkrZI7E-N5bPGbzp_UCbhsuDBRIJA8Kds092hmmwTdgwVc8U5vUW-ypgDVEzdgHDw_Atxj4NRU62nxEESTKtRQdU3nDPcLcYG3MDZdRIg","not-before-policy":0,"session_state":"246732f8-8bd8-41ee-a605-bc7d19e3fe28","scope":"openid email profile"} root@fedora1:/opt/keycloak1800 #$

Access Token Refresh Requests (grant_type=refresh_token)

After a client has obtained an access token / refresh token pair and has been presumably using the access token for ongoing requests, the access token will eventually expire (typically every 3 minutes). Rather than literally re-authenticating and having to re-collect a username / password from a human, the client can generate a new access token by posting the current refresh token back to the $token-uri endpoint with grant_type=refresh_token to generate a new access token and repeat this process ad infinitum.

The structure of the required request format is summarized below.

Request URLhttp://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token
MethodPOST
Body
client_id=DependsGUI&
client_secret=V3wpwsY31P7PcmUHzNVNWmWK6016q4FU
grant_type=refresh_token&
refresh_token=longrandomrefershtoken

The syntax of a curl command for executing this POST is shown below.


curl --verbose -d "client_id=DependsGUI" -d "grant_type=refresh_token" -d "refresh_token=longrandeomrefreshtokenstring" -d "client_secret=V3wpwsY31P7PcmUHzNVNWmWK6016q4FU" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token"

The output of the refresh request is shown below, looking just like the original response but NOTE both the access_token and refresh_token are new strings (look at the ends of the string reflecting new timestamps for the validity of the token).

mdh@fedora1:~/gitwork/dependsguioauth $ curl --verbose -d "client_id=DependsGUI" -d "grant_type=refresh_token" -d "refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2E4NGYzOC02ZDAyLTRhZDUtOTZmYS05Nzg5OGE4OGQxZDYifQ.eyJleHAiOjE2NTE3OTM3NzQsImlhdCI6MTY1MTc5MTk3NCwianRpIjoiMDFkMjllOTYtOTY1Yy00MDJiLTg1OGItNDc3OTIxODAzNWJiIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6Imh0dHA6Ly8xOTIuMTY4Ljk5LjEwOjgwMTEvcmVhbG1zL21kaGxhYnMiLCJzdWIiOiJiOWFmYTJhNy0xMzc0LTQzNjEtYjE4NS0yN2UwYjhiZWQ2OWYiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiRGVwZW5kc0dVSSIsInNlc3Npb25fc3RhdGUiOiJiOTllZWZmYi1lNjNkLTQ3YjItYTc0OC1hYWQwZGQ1YWZmMjYiLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJiOTllZWZmYi1lNjNkLTQ3YjItYTc0OC1hYWQwZGQ1YWZmMjYifQ.awst7vFeA1yuwzUeC3sPNPWwxYUWyKlmB_By-wxgF48" -d "client_secret=V3wpwsY31P7PcmUHzNVNWmWK6016q4FU" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token" * Trying 192.168.99.10:8011... * Connected to 192.168.99.10 (192.168.99.10) port 8011 (#0) > POST /realms/mdhlabs/protocol/openid-connect/token HTTP/1.1 > Host: 192.168.99.10:8011 > User-Agent: curl/7.71.1 > Accept: */* > Content-Length: 764 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 764 out of 764 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Referrer-Policy: no-referrer < X-Frame-Options: SAMEORIGIN < Strict-Transport-Security: max-age=31536000; includeSubDomains < Cache-Control: no-store < X-Content-Type-Options: nosniff < Pragma: no-cache < X-XSS-Protection: 1; mode=block < Content-Type: application/json < content-length: 2334 < {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NTE3OTM3ODAsImlhdCI6MTY1MTc5MzQ4MCwiYXV0aF90aW1lIjoxNjUxNzkxOTc0LCJqdGkiOiI2NmU2MzJlOC0wNDQzLTRmMTgtYmY1YS1kNjUyNmIzZTQ2YjIiLCJpc3MiOiJodHRwOi8vMTkyLjE2OC45OS4xMDo4MDExL3JlYWxtcy9tZGhsYWJzIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImI5YWZhMmE3LTEzNzQtNDM2MS1iMTg1LTI3ZTBiOGJlZDY5ZiIsInR5cCI6IkJlYXJlciIsImF6cCI6IkRlcGVuZHNHVUkiLCJzZXNzaW9uX3N0YXRlIjoiYjk5ZWVmZmItZTYzZC00N2IyLWE3NDgtYWFkMGRkNWFmZjI2IiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly8xOTIuMTY4Ljk5LjEwOjgwMjIiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLW1kaGxhYnMiLCJtZGhsYWJzdXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6ImI5OWVlZmZiLWU2M2QtNDdiMi1hNzQ4LWFhZDBkZDVhZmYyNiIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiQWwgRGlNZW9sYSIsInByZWZlcnJlZF91c2VybmFtZSI6Imd1aXVzZXIiLCJnaXZlbl9uYW1lIjoiQWwiLCJmYW1pbHlfbmFtZSI6IkRpTWVvbGEiLCJlbWFpbCI6Imd1aXVzZXJAbWRobGFicy5jb20ifQ.lGwlk90rQgjrfV2VAeUZhjVlTRGL3SOfEZ0PBd1V3SiL6LlEu6g1YAnTFV0cwTFEm-nEgWmvh9MZXRopS94g2SZitnDQbFL_4cpWgwXFW3cFH7PRkv_d04_5BEvK04gULci01-qyUZfxUsr9kHx6BPY6ZY9MWip54RL0YgoNjVDDawsTmQKegCZCrwarkG1VEUpxf9R9kCNuMYBa8wplOGxsRd-2TJKxi9KTggTVCX1lOQrzv2bWLjEnvyqBqf7KQusyzm21U9pPSMkd1Win8FSDBy6HeM7KSHPS9JzFz-oii_0V7h0t0JhQrD4T1XjQC6sadNbrdiETh7dRx4Slnw","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2E4NGYzOC02ZDAyLTRhZDUtOTZmYS05Nzg5OGE4OGQxZDYifQ.eyJleHAiOjE2NTE3OTUyODAsImlhdCI6MTY1MTc5MzQ4MCwianRpIjoiNjE2NzQ0ZDctOGNiNS00OWQyLWFiNGMtNDYwYTExYzM2YTMzIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6Imh0dHA6Ly8xOTIuMTY4Ljk5LjEwOjgwMTEvcmVhbG1zL21kaGxhYnMiLCJzdWIiOiJiOWFmYTJhNy0xMzc0LTQzNjEtYjE4NS0yN2UwYjhiZWQ2OWYiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiRGVwZW5kc0dVSSIsInNlc3Npb25fc3RhdGUiOiJiOTllZWZmYi1lNjNkLTQ3YjItYTc0OC1hYWQwZGQ1YWZmMjYiLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJiOTllZWZmYi1lNjNkLTQ3YjItYTc0OC1hYWQwZGQ1YWZmMjYifQ.SgGlKQooxL7Uu1QQlG8tBsYXxofDw7VgZJsKgR2iXTk","token_type":"Bearer","not-before-policy":0,"session_state":"b99eeffb-e63d-47b2-a748-aad0dd5aff26","scope":"email profile"}mdh@fedora1:~/gitwork/dependsguioauth $

Access Token Requests (grant_type=client_credentials)

When generating access tokens for a software process that is in the same administrative / security domain as the Authorization Server, it is common to use a grant_type=client_credentials flow instead of grant_type=authorization_code flow since communication between the application and AS is presumed to be secured and a third party isn't having to delegate authorizations without disclosing their end-user username / password.

The structure of the required request format is summarized below.

Request URLhttp://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token
MethodPOST
Body
client_id=DependsClientBasic&
client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH
grant_type=client_credentials

Note that a redirect_uri parameter is not required because the process submitting this request is presumed to be the user of the resulting access token and it doesn't need to redirect through any other process to reach the application component needing the access token.


Examples of the tokens returned for the two machine userids are shown below.

mdh@fedora1:~/gitwork/oauthdependsclient $ curl -d "client_id=DependsClientBasic" -d "grant_type=client_credentials" -d "client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token" {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NTYxODMwMjUsImlhdCI6MTY1NjE4MjY2NSwianRpIjoiYmY4MDZhMjAtZjllNS00MjQ3LWJlMDYtNzJiODkzMjQ1NGJmIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJzZXJ2aWNlLWFjY291bnQtZGVwZW5kc2NsaWVudGJhc2ljIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiRGVwZW5kc0NsaWVudEJhc2ljIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly8xOTIuMTY4Ljk5LjEwOjgwODAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwibWRobGFic2dldG9ubHkiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtbWRobGFicyJdfSwicmVzb3VyY2VfYWNjZXNzIjp7IkRlcGVuZHNDbGllbnRCYXNpYyI6eyJyb2xlcyI6WyJ1bWFfcHJvdGVjdGlvbiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJEZXBlbmRzQ2xpZW50QmFzaWMiLCJjbGllbnRIb3N0IjoiMTkyLjE2OC45OS4xMCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJtZGhsYWJzZ2V0b25seSIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIl0sInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1kZXBlbmRzY2xpZW50YmFzaWMiLCJjbGllbnRBZGRyZXNzIjoiMTkyLjE2OC45OS4xMCJ9.NEpzzmO7frq4CjObBO4yK6KA-uHztbiXxD_r0LWVvtiqk8C5uVjUzYQroKRGOyoUmYm4PTW4uJyztuJkdl7P3ITTIy85eiHZdLyy_XHRbVklijwYmB22vJefb8toHY2zklpxJ90o0uWhQ3FpVeIDbR6X51uP_bzFpZo07gnxx3bxyU8RI_VR4tJtEekPcF5k9SYNAl_Y4mPjvs0IXNFKMYb_wNaLV2OJOBZFAH1uRbF44hubcqYez-aWPc_0Vk1G2wCMze7BdCFuVHtrsE53ExzUeKc5rHSrLw7Xt_JDUtW58rKAApPxBpa-VwhlOa-ZHtB6DuSELUSkxO4gqEv-pQ","expires_in":360,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"email profile"} mdh@fedora1:~/gitwork/oauthdependsclient $ mdh@fedora1:~/gitwork/oauthdependsclient $ curl -d "client_id=DependsClientFull" -d "grant_type=client_credentials" -d "client_secret=ediQAgqkh4fwaOP5t5vaHLElj29fskSz" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token" {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NTYxODI4NzcsImlhdCI6MTY1NjE4MjUxNywianRpIjoiYTE3NTM2MGUtYTgyOC00YzQ4LWI1YWItMjBkZjY1MTI1ZWFjIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI5MjNjM2UxNi01MDMyLTRkZjctYTg1MS1jNjBmYmQ1ZmZjODIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtbWRobGFicyIsIm1kaGxhYnNmdWxsIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImNsaWVudEhvc3QiOiIxOTIuMTY4Ljk5LjEwIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LWRlcGVuZHNjbGllbnRmdWxsIiwiY2xpZW50QWRkcmVzcyI6IjE5Mi4xNjguOTkuMTAifQ.grqUJEEGmbGHHMyHVnMNv6lTffCln4hYYt8W49cTVIXKgs86tvKWOrdbSwUCrvBjnnvRpKv5g8cDtU9AJyhDeu6Ysuo5orbZas3konkWcOYTKDMRwfZFaM9aCyNEH2LSf8aMBp4oYemgmMgYA9zxkM1lxlKifb3ybUbyKj4IALEIXgVyxPuO4FK-kPFq-J2PaX8LXTEIiQJzUnoVKntGEDn4klJr7A5nKWHVlicxRcqDIR0P72r_AtmqoRYaBL6g5U42IcKx99M-NXrCfQ1g4gqtpaCUJNrmZaxdNbRefDm5akr0wmfYISS8WmzP4Y1Gef-UIvJooKe_WEka61WQJw","expires_in":360,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"email profile"} mdh@fedora1:~/gitwork/oauthdependsclient $

Unlike token responses for authorization_code and password scenarios, responses for client_credentials scenarios do NOT include a refresh token value. The client_credentials scenario assumes the "client" is a machine with a secure environment which can house the client_id and client_secret securely so as a token expires, the client can simply request a new one with the same static credential from the Authorization Server. The authorization_code and password flows assume a HUMAN is involved who should not be bothered to re-enter a username / password combination so a refresh token is returned to simplify renewing the access token without re-authentication by the human.

Access Token Requests (grant_type=password)

The grant_type=password flow is conceptually similar to the grant_type=client_credential flow but was originally provided for scenarios where authorization is required for a specific human user whose userid / password is already collected by the client application and is administered in the same security domain as the applicatioin. However, use of this flow is STRONGLY discouraged and references to it are being stripped from OAUth2 standards documentation which means vendor support for it may vary or disappear.

The structure of the required request format is summarized below.

Request URLhttp://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token
MethodPOST
Body
client_id=DependsGUI&
client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH&
username=dependsgetonly&
password=weakpassword&
grant_type=password

Note that a redirect_uri parameter is not required because the process submitting this request is presumed to be the user of the resulting access token and it doesn't need to redirect through any other process to reach the application component needing the access token.

Examples of the tokens returned for the two userids are shown below.



Validating JTW Tokens from Keycloak using jwt.io

Responses from Authorization Servers are typically JSON encoded with the actual token value further encoded in base64 and sometimes further encrypted. The actual base64 token strings can be conveniently decoded for troubleshooting and insight using the tool at https://jwt.io which is operated by one of the key firms contributing code for JWT and OAuth support. If only interested in the token paramters, simply copy the token from a command line and paste it into the left hand side of the page. The utility will automatically identify the encoding algorithm and decode the contents in three color coded sections as shown below for an access token.


{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "CThW2f_Ko7WKAg6Ck2lCD2hhtO9Rfo2Wf1tNrdtzDiA"
}
{
  "iat": 1654106542,
  "jti": "8f4862f2-dd04-49e2-854a-c5d00744b5fa",
  "iss": "http://192.168.99.10:8011/realms/mdhlabs",
  "aud": "account",
  "sub": "dependsgetonly",
  "typ": "Bearer",
  "azp": "KuberDependsOAuth",
  "session_state": "52cffb42-e26e-41f1-bd5b-9099bccb509c",
  "allowed-origins": [
    "http://192.168.99.10:8080"
  ],
  "realm_access": {
    "roles": [
      "offline_access",
      "mdhlabsgetonly",
      "uma_authorization",
      "default-roles-mdhlabs"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "email profile",
  "sid": "52cffb42-e26e-41f1-bd5b-9099bccb509c",
  "email_verified": false,
  "roles": [
    "offline_access",
    "mdhlabsgetonly",
    "uma_authorization",
    "default-roles-mdhlabs"
  ],
  "name": "Eric Clapton",
  "preferred_username": "dependsgetonly",
  "given_name": "Eric",
  "family_name": "Clapton",
  "email": "slowhand@rsorecords.com"
}
In the example above, note how the roles : [ ] collection within "realm roles" shown in green is duplicated as a top level Json item below in blue. That does not happen by default but is due to the configuration in section 3.3.8 which triggers Keycloak to return the "realm roles" content as a second top-level "roles" claim. This is not strictly necessary but as will be shown later, makes coding logic to transform external claims into application authorities easier.


If the overall certificate chain used for the encoding needs to be validated, the private key auto-generated within Keycloak and the matching public key for the realm can be copied out of the Keycloak admin console using the buttons shown below and pasted into the blue forms at the bottom of the page (which will reset the token input field). The token can be repasted into the token field and the bottom left of the screen will change from Invalid Signature to Valid Signature.


Accessing Additional User Information via /userinfo Endpoint

In the conceptual design of OAuth, after an application is provided an authorization code from a user and converts it to an access token, that access token can serve as a "bearer token" for all subsequent application access. However, specific implementations vary on how much USER information is returned encoded within access tokens for the application to use. Essentially, an access token says "trust me, this person is allowed" but it may not convey WHO THE PERSON IS. This is obviously not desirable for applications required to associate all actions with a userid and associated contact information. If you build an app that allows authentication via Google and Facebook, you may still need a user's email as a unique login string for logs in your application.

The userInfo-uri endpoint is intended to allow this extra USER information to be returned for a supplied active access token. Whether this extra call is strictly required varies by provider. By default, realms in keycloak can return username and email information with the access token. They can also return it on the separate userinfo-uri endpoint. Other providers only return an access access token and require other user information to be fetched from an alternate endpoint.

After an application interacts with the Authorization Server, the resulting bearer token will provide access to appropriately mapped resources for the lifetime of the token to ANY PROCESS in possession of that token. The token itself conveys no USER information of the identity that obtained the token. Many scenarios will require that so the token can be submitted back to the Authorization Server on its /userinfo endpoint to retrieve a JSON representation of other user paramaters associated with the token.

In some cases, the Open Connect ID libraries will discover the reqired URI endpoints on the Authorization Server automatically but in other cases, the URL may require explicity configuration. It is a good idea to manually test access to that endpoint. The endpoint information for Keycloak is summarized below. Other providers require similar parameters on their userinfo-uri endpoint.

Request URLhttp://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/userinfo
MethodGET
Body
(none)
Header
Authorization: Bearer $accesstoken

The process is illustrated below.

mdh@fedora1:~/gitwork/oauthdependsresource $ FULLTOKEN=$(curl -d "client_id=KuberDependsOAuth" -d "username=dependsfull" -d "password=weakpassword" -d "grant_type=password" -d "client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token" | jq -r '.access_token') % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2590 100 2453 100 137 19782 1104 --:--:-- --:--:-- --:--:-- 20887 mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $ curl -H "Content-type: application/json" -H "Authorization: Bearer $FULLTOKEN" -X GET http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/userinfo {"sub":"2ac174d6-e819-4104-8109-b61140272c2d", "email_verified":true, "roles":["offline_access","uma_authorization", "default-roles-mdhlabs","mdhlabsfull"], "name":"Jeff Beck", "preferred_username":"dependsfull", "given_name":"Jeff", "family_name":"Beck","email":"truth@epicrecords.com"} mdh@fedora1:~/gitwork/oauthdependsresource $

Examining Opaque Tokens via /introspect Endpoint

When opaque tokens are used, the payload of the original bearer token does not directly decode to the underlying authorization and user information. It only provides a foreign key into a remote repositiory owned by the Authorization Server (or a downstream Identity Provider) so if user parameters are required for an opaque token, the client must post the Oauth bearer token along with the original client-id and client-secret used when generating it to the /introspect endpoint of the Authorization Server.

Request URLhttp://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/userinfo
MethodGET
Body
client_id=DependsGUI&
client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH
token=currentaccesstoken

The endpoint actually works for opaque and non-opaque tokens so it is demonstrated below using the same token generated in prior tests.

mdh@fedora1:~/gitwork/oauthdependsresource $ FULLTOKEN=$(curl -d "client_id=KuberDependsOAuth" -d "username=dependsfull" -d "password=weakpassword" -d "grant_type=password" -d "client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token" | jq -r '.access_token') % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2590 100 2453 100 137 19782 1104 --:--:-- --:--:-- --:--:-- 20887 mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $ curl -d "token=$FULLTOKEN" -d "client_id=KuberDependsOAuth" -d "client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH" http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token/introspect {"exp":1650852164,"iat":1650851864,"jti":"dfd00241-1092-4f3f-8c05-b2eed4dd1707","iss":"http://192.168.99.10:8011/realms/mdhlabs","aud":"account","sub":"2ac174d6-e819-4104-8109-b61140272c2d","typ":"Bearer", "azp":"KuberDependsOAuth","session_state":"bca8acdb-cead-43aa-b3b2-d50ff81d01ca","name":"Jeff Beck","given_name":"Jeff", "family_name":"Beck", "preferred_username":"dependsfull", "email":"truth@epicrecords.com", "email_verified":true,"acr":"1", "allowed-origins":["http://192.168.99.10:8080"], "realm_access":{"roles":["offline_access","uma_authorization","default-roles-mdhlabs","mdhlabsfull"]}, "resource_access":{"account":{"roles":["manage-account","manage-account-links","view-profile"]}}, "scope":"email profile","sid":"bca8acdb-cead-43aa-b3b2-d50ff81d01ca", "roles":["offline_access","uma_authorization","default-roles-mdhlabs","mdhlabsfull"], "client_id":"KuberDependsOAuth", "username":"dependsfull", "active":true} mdh@fedora1:~/gitwork/oauthdependsresource $

If the /introspect endpoint is invoked using a token that is expired, a very short response will be returned.


mdh@fedora1:~/gitwork/oauthdependsresource $ curl -d "token=$FULLTOKEN" -d "client_id=KuberDependsOAuth" -d "client_secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH" http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token/introspect {"active":false} mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $