Skip to content

A conversation

Janne Lauros edited this page Jan 9, 2019 · 53 revisions

This section will walk you through what might happen when a user wants to use OIDC to authenticate/authorize and the Relying Party (RP) has never seen the OpenID Connect Provider (OP) before. This is an example of how dynamic the interaction between an RP and an OP can be using OIDC.

We start from knowing absolutely nothing, having to use WebFinger to find the OP. Then follows dynamic provider info discovery and client registration before the user can be brought in and do the authentication/authorization bit. And lastly the RP will ask for an access token and after that information about the user.

Initial setup

We prepare the initial setup for widely used and one of the simplest cases to configure. The RP does not assume OP to encrypt anything so we do not have to even publish any keys.

ServiceContext

Which is where information common to more then one service is kept. Initally all we need to set is redirect uri.

ServiceContext serviceContext = new ServiceContext();
context.setRedirectUris(Arrays.asList("https://your.rp.example.com/example/callback"));

State Database instance

For this example we have an in-memory data store.

State stateDb=new InMemoryStateImpl();

Webfinger

We will use WebFinger (RFC7033) to find out where we can learn more about the OP. What we have to start with is an user identifier provided by the user. The identifier we got was: [email protected]. With this information we can do:

Map<String, Object> requestParams = new HashMap<>();
requestParams.put(Constants.WEBFINGER_RESOURCE, "[email protected]");
Webfinger webfinger = new Webfinger(serviceContext, null);
HttpArguments httpArguments =  webfinger.getRequestParameters(requestParams);

Running the method getRequestParameters will return the information necessary to do a HTTP request. In this case the value of httpArguments.getUrl() will be:

'https://example.com/.well-known/webfinger?resource=acct%3Afoobar%40example.com&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer'

as you can see the getRequestParameters constructed a URL that can be used to get the wanted information. Doing HTTP GET on this URL will return a JSON document that looks like this:

{
  "subject": "acct:[email protected]",
  "links": [{"rel": "http://openid.net/specs/connect/1.0/issuer",
             "href": "https://example.com"}],
  "expires": "2018-02-04T11:08:41Z"
}

To parse it (String variable response) and use it I can run another method provide by the service instance:

Message message = webfinger.parseResponse(response);

It’s assumed that message contains the JSON document mentioned above. Method parseResponse only parses the response. So apart from that method we also need to invoke updateServiceContext:

webfinger.updateServiceContext(message);

The result of this is that the information in serviceContext will change. We now have this:

serviceContext.getIssuer(): "https://example.com"

And that is all we need to fetch the provider info.

Provider info discovery

We use the same process as with webfinger but with another service instance:

ProviderInfoDiscovery discovery = new ProviderInfoDiscovery(serviceContext, null, null);
httpArguments =  discovery.getRequestParameters(null);

httpArguments.getUrl() will now contain

'https://example.com/.well-known/openid-configuration'

And this is the first example of magic that you will see.

getRequestParameters knows how to contruct the OpenID Connect providers discovery URL from information stored in the serviceContext instance. Now, you could have done this without webfinger by setting issuer value with method serviceContext.setIssuer("https://example.com") by yourself.

Doing HTTP GET on the provided URL should get us the provider info. We get a JSON document that looks something like this:

{
"version": "3.0",
"token_endpoint_auth_methods_supported": [
    "client_secret_post", "client_secret_basic",
    "client_secret_jwt", "private_key_jwt"],
"claims_parameter_supported": True,
"request_parameter_supported": True,
"request_uri_parameter_supported": True,
"require_request_uri_registration": True,
"grant_types_supported": ["authorization_code",
                          "implicit",
                          "urn:ietf:params:oauth:grant-type:jwt-bearer",
                          "refresh_token"],
"response_types_supported": ["code", "id_token",
                             "id_token token",
                             "code id_token",
                             "code token",
                             "code id_token token"],
"response_modes_supported": ["query", "fragment",
                             "form_post"],
"subject_types_supported": ["public", "pairwise"],
"claim_types_supported": ["normal", "aggregated",
                          "distributed"],
"claims_supported": ["birthdate", "address",
                     "nickname", "picture", "website",
                     "email", "gender", "sub",
                     "phone_number_verified",
                     "given_name", "profile",
                     "phone_number", "updated_at",
                     "middle_name", "name", "locale",
                     "email_verified",
                     "preferred_username", "zoneinfo",
                     "family_name"],
"scopes_supported": ["openid", "profile", "email",
                     "address", "phone",
                     "offline_access", "openid"],
"userinfo_signing_alg_values_supported": [
    "RS256", "RS384", "RS512",
    "ES256", "ES384", "ES512",
    "HS256", "HS384", "HS512",
    "PS256", "PS384", "PS512", "none"],
"id_token_signing_alg_values_supported": [
    "RS256", "RS384", "RS512",
    "ES256", "ES384", "ES512",
    "HS256", "HS384", "HS512",
    "PS256", "PS384", "PS512", "none"],
"request_object_signing_alg_values_supported": [
    "RS256", "RS384", "RS512", "ES256", "ES384",
    "ES512", "HS256", "HS384", "HS512", "PS256",
    "PS384", "PS512", "none"],
"token_endpoint_auth_signing_alg_values_supported": [
    "RS256", "RS384", "RS512", "ES256", "ES384",
    "ES512", "HS256", "HS384", "HS512", "PS256",
    "PS384", "PS512"],
"userinfo_encryption_alg_values_supported": [
    "RSA1_5", "RSA-OAEP", "RSA-OAEP-256",
    "A128KW", "A192KW", "A256KW",
    "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"],
"id_token_encryption_alg_values_supported": [
    "RSA1_5", "RSA-OAEP", "RSA-OAEP-256",
    "A128KW", "A192KW", "A256KW",
    "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"],
"request_object_encryption_alg_values_supported": [
   "RSA1_5", "RSA-OAEP", "RSA-OAEP-256", "A128KW",
   "A192KW", "A256KW", "ECDH-ES", "ECDH-ES+A128KW",
   "ECDH-ES+A192KW", "ECDH-ES+A256KW"],
"userinfo_encryption_enc_values_supported": [
    "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512",
    "A128GCM", "A192GCM", "A256GCM"],
"id_token_encryption_enc_values_supported": [
    "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512",
    "A128GCM", "A192GCM", "A256GCM"],
"request_object_encryption_enc_values_supported": [
    "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512",
    "A128GCM", "A192GCM", "A256GCM"],
"acr_values_supported": ["PASSWORD"],
"issuer": "https://example.com",
"jwks_uri": "https://example.com/static/jwks_tE2iLbOAqXhe8bqh.json",
"authorization_endpoint": "https://example.com/authorization",
"token_endpoint": "https://example.com/token",
"userinfo_endpoint": "https://example.com/userinfo",
"registration_endpoint": "https://example.com/registration",
"end_session_endpoint": "https://example.com/end_session"}

Quite a lot of information as you can see. We feed this information into parseResponse and updateServiceContext and let them do their business:

message = discovery.parseResponse(response);
discovery.updateServiceContext(message, null);

Variable response contains the JSON document from the HTTP response. parseResponse will parse and verify the response. One such verification is to check that the value provided as issuer is the same as the URL used to fetch the information without the ‘.well-known’ part. In our case the exact value that the webfinger query produced.

As with the webfinger service updateServiceContext adds things to serviceContext. So we now have for example:

serviceContext.getProviderConfigurationResponse().getClaims().get("issuer"): https://example.com
serviceContext.getProviderConfigurationResponse().getClaims().get("authorization_endpoint"): https://example.com/authorization

Client registration

By now you should recognize the pattern:

Registration registration = new Registration(serviceContext, null, null);
httpArguments = registration.getRequestParameters(null);

Now httpArguments contains 3 relevant parts:

  • httpArguments.getUrl(): The URL to which the HTTP request should be sent
  • httpArguments.getBody(): A JSON document that should go in the body of the HTTP request
  • httpArguments.getHeader().getContentType(): HTTP content type header to be used with the request

and we got:

url: 'https://example.com/registration'
body: '{
    "application_type": "web",
    "response_types": ["code"],
    "contacts": ["[email protected]"],
    "jwks_uri": "https://example.org/static/jwks.json",
    "token_endpoint_auth_method":
    "client_secret_basic",
    "redirect_uris": ["https://example.org/authz_cb"]
    }'
content type: 'application/json'

The information in the body comes from the client configuration. If we use this information and do HTTP POST to the provided URL we will receive a response like this:

{
"client_id": "zls2qhN1jO6A",
"client_secret": "c8434f28cf9375d9a7f3b50dcfdf6a20d6e702e310066874f794817f",
"registration_access_token": "NdGrGR7LCuzNtixvBFnDphGXv7wRcONn",
"registration_client_uri": "https://localhost:8080/oicrp/registration?client_id=zls2qhN1jO6A",
"client_secret_expires_at": 1517823388,
"client_id_issued_at": 1517736988,
"application_type": "web",
"response_types": ["code"],
"contacts": ["[email protected]"],
"token_endpoint_auth_method": "client_secret_basic",
"redirect_uris": ["https://example.com/authz_cb"]
}

Again a JSON document. This is the OP’s response to the RP’s registration request.

We feed the response to parseResponse which will parse, verify and interpret the response and then we update the serviceContext.

message = registration.parseResponse(response);
registration.updateServiceContext(message);

The information stored in serviceContext is mostly under serviceContext.getRegistrationResponse() but some, more important, will be stored at a directly reachable place:

serviceContext.getClientId(): zls2qhN1jO6A
serviceContext.getClientSecret(): c8434f28cf9375d9a7f3b50dcfdf6a20d6e702e310066874f794817f

By that we have finalized the dynamic discovery and registration now we can get down to doing the authentication/authorization bits.

Authorization

In the following example I’m using code flow since that allows me to show more of what the javaOIDCService project can do.

Like when I used the other services this one is no different except for stateDb is now needed:

Authentication authentication = new Authentication(serviceContext, stateDb, null);
httpArguments =  authentication.getRequestParameters(null);

httpArguments will only contain one piece of data and that is a URL:

httpArguments.getUrl(): https://example.com/authorization?state=Oh3w3gKlvoM2ehFqlxI3HIK5&nonce=UvudLKz287YByZdsY3AJoPAlEXQkJ0dK&response_type=code&client_id=zls2qhN1jO6A&scope=openid&redirect_uri=https%3A%2F%2Fexample.org%2Fauthz_cb

And the source for all that information was:

  • The authorization endpoint comes from the dynamic provider info discovery,
  • client_id from the client registration,
  • response_type, scope and redirect_uri from the client configuration and
  • state and nonce are dynamically created by the service instance.

When this service instance creates a request it will also create a session instance in stateDb keyed on the state value.

I do HTTP GET on the provided URL and will eventually get redirected back to the RP with the response in the query part of the redirect URL. Below you have just the query component:

state=Oh3w3gKlvoM2ehFqlxI3HIK5&scope=openid&code=Z0FBQUFBQmFkdFFjUVpFWE81SHU5N1N4N01aQWJ1Y3Y1MWFfMTVXXzhEcll2a0lkd0Z2Qk9lOHYtTUZjRnRjUzhNc1FOdm9RMGJ5aXhNUUtYSkdldTItRnBFVFV5YkhIVE5Gbk1VY2x2YmRuQXhxTEFSV2d6Zi1IaHE3SklpdndGbzRHR2tfT0Rwck5RTW1TalRwRUg0SE5JSUJtSC1lZU5HTXRjdkZXWXUzT3VodF8tdFhtX2NURFNiRXVhX1pFTFk1SXZ6NWhvSEdyXzNQRXVfZU9uTS1GZnB1dnVkYmRZSkh4VDdPWENlQ240al9GSkdFa1I0Yz0%3D&iss=https%3A%2F%2Fexample.com&client_id=zls2qhN1jO6A

I feed the HttpServletRequest request url and query parameters into the parseResponse method of the authentication service instance and hope for the best. Notice that from now on we start to update service context per stateKey.

message = authentication.parseResponse(request.getRequestURL() + "?" + request.getQueryString());
String stateKey=(String) message.getClaims().get("state");
authentication.updateServiceContext(message, stateKey);

Access token

When sending an access token request I have to use the correct authorization code value. Code value with authentication response is stored to stateDb by state key value. We need to set state as pre constructor argument:

AccessToken accessToken = new AccessToken(serviceContext, stateDb, null);
Map<String, Object> preConstructorArgs = new HashMap<String, Object>(); 
preConstructorArgs.put("state", stateKey);
accessToken.setPreConstructorArgs(preConstructorArgs);
httpArguments =  accessToken.getRequestParameters(null);

The OIDC standard says that the redirect_uri used for the authorization request should be provided in the access token request, therefore the service will add it if I don’t.

This time httpArguments has these four parts:

httpArguments.getUrl(): https://example.com/token
httpArguments.getBody(): grant_type=authorization_code&state=Oh3w3gKlvoM2ehFqlxI3HIK5&redirect_uri=https%3A%2F%2Fexample.org%2Fauthz_cb&code=Z0FBQUFBQmFkdFFjUVpFWE81SHU5N1N4N01aQWJ1Y3Y1MWFfMTVXXzhEcll2a0lkd0Z2Qk9lOHYtTUZjRnRjUzhNc1FOdm9RMGJ5aXhNUUtYSkdldTItRnBFVFV5YkhIVE5Gbk1VY2x2YmRuQXhxTEFSV2d6Zi1IaHE3SklpdndGbzRHR2tfT0Rwck5RTW1TalRwRUg0SE5JSUJtSC1lZU5HTXRjdkZXWXUzT3VodF8tdFhtX2NURFNiRXVhX1pFTFk1SXZ6NWhvSEdyXzNQRXVfZU9uTS1GZnB1dnVkYmRZSkh4VDdPWENlQ240al9GSkdFa1I0Yz0%3D&client_id=zls2qhN1jO6A
httpArguments.getHeader().getAuthorization(): 'Basic emxzMnFoTjFqTzZBOmM4NDM0ZjI4Y2Y5Mzc1ZDlhN2YzYjUwZGNmZGY2YTIwZDZlNzAyZTMxMDA2Njg3NGY3OTQ4MTdm'
httpArguments.getHeader().getContentType(): 'application/x-www-form-urlencoded'

Url was picked from the discovered provider info. The Authorization header looks like it does because the default client authentication method is defined to be ‘client_secret_basic’. The body is, a bit surprising but according to the standard, urlencoded.

The response has this JSON document in the body:

{
'state': 'Oh3w3gKlvoM2ehFqlxI3HIK5',
'scope': 'openid',
'access_token': 'Z0FBQUFBQmFkdFFjc0hyU2lialZyUkhvQjliUjU2R3hTQWZ4cDZFMnRTdGxkV3VoQmppZllyN2htWHlhU2Ria0tRV2NqcjEwOG5acWEzbzR3ZUNYTlFGTUJ6T1hpOGhzZE5UTndaYV9WcmJBdFcwTmRIWjJPZXlKUHBXWVYteEM3aE9BMGF1YWQyeVZiZGVZZExtOGpHT1dpMHNVUzRCMkdFRVFROHJIMkNTdUp0X0xlWHlMeGRJUTh5cW5LMFF3ZG5FbzBpbWlrTFUxcFkzbG9ORl92cll1MC02RjFZMDBNbnB4enpNcHVEMXRxSmtHSEtWQXlrTT0=',
'token_type': 'Bearer',
'id_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IlEwMl92cXJIYlFpRk5kemZ4aFhUblhpMWphemZhTlFJMlNNa2NvNmMxdFEifQ.eyJpc3MiOiAiaHR0cHM6Ly9sb2NhbGhvc3Q6ODA4MC9vaWNycC9ycC11c2VyaW5mby1iZWFyZXItaGVhZGVyIiwgInN1YiI6ICIxYjJmYzkzNDFhMTZhZTRlMzAwODI5NjVkNTM3YWU0N2MyMWEwZjI3ZmQ0M2VhYjc4MzMwZWQ4MTc1MWFlNmRiIiwgImF1ZCI6IFsiemxzMnFoTjFqTzZBIl0sICJleHAiOiAxNTE3ODIzMzg4LCAiYWNyIjogIlBBU1NXT1JEIiwgImlhdCI6IDE1MTc3MzY5ODgsICJhdXRoX3RpbWUiOiAxNTE3NzM2OTg4LCAibm9uY2UiOiAiVXZ1ZExLejI4N1lCeVpkc1kzQUpvUEFsRVhRa0owZEsifQ.cOJYa-yNeVgHeitol2Zw3Z3TYh9Fxys8BwAmACSZEYzwNnt1DwSfvhLTOeSFcAh2vsrvmNh2HqOy4plnH5-uB-KIEJY3E9GTmmK5uZDGvtSfMXqq2M45MA-71lJx2xrWwE5aH59WWJkEOY9s-gl0KJyMh7VFFP-B86d_16rg2hB6y9ajH5ieR9mc_E0RdwZVDLF_uBcWj0tLiTH2AaZK4akCAiFUant261M2OQnreJ7D6WPfZl_UHYPCm_6nhazvrQuovj9ahxAnqkg3UFBSycX4qr1brfi1Ak-xKRdTQ08NYJwtC8JVxSM0ic3E2XsOIW0hThofKwQUiolWW4yq0Q',
}

We will deal with this in the now well known fashion:

message = accessToken.parseResponse(response, stateKey);
accessToken.updateServiceContext(message, stateKey);

Note that we need to provide the methods with the stateKey parameter so they will know where to find the correct information needed to verify the response and later store the received information.

TODO Python version stores verified id token to stateDb and that is presented in this phase of documentation.

User info

Again we have to provide the pre constructor method with the correct state value:

UserInfo userInfo = new UserInfo(serviceContext, stateDb, null);
preConstructorArgs = new HashMap<String, Object>(); 
preConstructorArgs.put("state", stateKey);
userInfo.setPreConstructorArgs(preConstructorArgs);
httpArguments =  userInfo.getRequestParameters(null);

And the response is a JSON document:

{"sub": "1b2fc9341a16ae4e30082965d537ae47c21a0f27fd43eab78330ed81751ae6db"}

Only the sub parameter because the asked for scope was ‘openid’.

Parsing, verifying and storing away the information is done the usual way:

message = userInfo.parseResponse(response, stateKey);
userInfo.updateServiceContext(message, stateKey);

And we are done !! :-)

In the stateDb we have the following information:

AuthenticationRequest authRequest=stateDb.getItem(stateKey, MessageType.AUTHORIZATION_REQUEST);
AuthenticationResponse authResponse=stateDb.getItem(stateKey, MessageType.AUTHORIZATION_RESPONSE);
TokenResponse tokenResponse=stateDb.getItem(stateKey, MessageType.TOKEN_RESPONSE);
IDToken iDToken=(IDToken)stateDb.getItem(stateKey, MessageType.VERIFIED_IDTOKEN);
OpenIDSchema userInfo=stateDb.getItem(stateKey, MessageType.USER_INFO);
Clone this wiki locally