Getting a refresh token from CSS

Hi all,

I am trying to get a refresh token from the server when authenticating a client. Following is my authentication request.

http.post(client.issuer.tokenEndpoint,
    headers: {
      'Accept': '*/*',
      'Accept-Encoding': 'gzip, deflate, br',
      'DPoP': dPoPToken,
      'content-type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic $h',
      'Connection': 'keep-alive',
    },
    body: {
      'grant_type': 'authorization_code',
      'code': code,
      'redirect_uri': redirectUri.toString(),
      'Scope': 'openid profile offline_access',
      'client_id': client.clientId,
      'code_verifier': _proofKeyForCodeExchange['code_verifier']
    }

With solidcommunity.net I am able to get a refresh token out. But with my own CSS installation, I cannot seem to get the refresh token. The server does not respond with a refresh token. Do I need to set up a specific configuration when installing the server in order to get a refresh token upon authentication?

Any help is much appreciated.

Thanks, Anushka

Hi @Anushka !

I’m not very familiar with CSS configuration, but I can help with the OpenID part of this. The issue you are seeing happens at the token request step, after the authorization request, as part of the authorization code flow (correct me if any of this is inaccurate).

Is the client you are using statically registered, or dynamically ? In other words, how is client.clientId obtained? As part of the client registration (be it static or dynamic), metadata about the client is provided, including the authorized scopes for the client. Make sure that offline_access is present at this point, because this is the scope that causes the OpenID Provider to issue a Refresh Token.

Also, in your example, the POST body has a Scope key (it should probably be scope), however that isn’t a parameter provided during the token request, it should be provided during the authorization request according to the specification. Can you share more about the complete OpenID flow you are following?

1 Like

Hi @zwifi thanks a lot for your reply. I do dynamic client registration. So based on your answers, here’s what I do now.

Dynamic client registration

final response = await http.post(Uri.parse(regEndPoint),
    headers: <String, String>{
      'Accept': '*/*',
      'Content-Type': 'application/json',
      'Connection': 'keep-alive',
      'Accept-Encoding': 'gzip, deflate, br',
    },
    body: json.encode({
      "application_type": "web",
      "scope": "openid, profile, offline_access",
      "grant_types": ["authorization_code", "refresh_token"],
      "redirect_uris": reidirUrlList,
      "token_endpoint_auth_method": "client_secret_basic",
    }));

The response I get for this registration request is as follows:

response = {"application_type":"web",
"grant_types":["authorization_code","refresh_token"],
"id_token_signed_response_alg":"ES256",
"require_auth_time":false,
"response_types":["code"],
"subject_type":"public",
"token_endpoint_auth_method":"client_secret_basic",
"post_logout_redirect_uris":[],
"require_pushed_authorization_requests":false,
"dpop_bound_access_tokens":false,
"client_id_issued_at":1710377438,
"client_id":"eGKzO3ArtyRq5Fl2",
"client_secret_expires_at":0,"client_secret":"G2uxR3VA7D...",
"redirect_uris":["http://localhost:4400/"],
"scope":"openid profile offline_access",
"registration_client_uri":"https://solidserver.url/.oidc/reg/eGKzO3ArtyRq5Fl2",
"registration_access_token":"d3rlVyYJFlfAZ7qMO..."}

I then create a client object using the above response details and after that I do the Authorisation request as follows:

var h = base64.encode('${client.clientId}:${client.clientSecret}'.codeUnits);
json = await http.post(client.issuer.tokenEndpoint,
    headers: {
      'Accept': '*/*',
      'Accept-Encoding': 'gzip, deflate, br',
      'content-type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic $h',
      'Connection': 'keep-alive',
    },
    body: {
      'grant_type': 'refresh_token',
      'code': code,
      'redirect_uri': redirectUri.toString(),
      'client_id': client.clientId,
      'code_verifier': _proofKeyForCodeExchange['code_verifier']
    },
    client: client.httpClient);

But when I do this now I get the following error.

Unhandled Exception: OpenIdException(invalid_request): missing required parameter 'refresh_token'

When I change the grant_type in the Authorisation request to authorization_code, the request goes through, but I still do not get a refresh_token.

I am sure I am doing something wrong with these requests, but could not figure out exactly what. Any help would be much appreciated.

1 Like

The authorization request shouldn’t be a POST to the Token Endpoint, but rather a GET to the authorization endpoint. This will redirect your user to the OpenID Provider (OP), where they will log in using whatever credentials the OP supports (typically, username/password, but it could be anything, that’s totally transparent to the Relying Party (RP), i.e. your app). Then, the OP will redirect the user back to the redirect_url that has been provided as part of the Authorization Request (if it matches the one registered during Dynamic Client Registration), providing the authorisation code. It’s only at this stage that you can do the Token Request, i.e. the POST request to the Token Endpoint, using the authorization code you’ve just been sent by the OP. If everything goes well, the response from the Token Endpoint will include an ID Token, an Access Token, and a Refresh Token.

If you aren’t too familiar with OAuth in general and/or OpenID Connect in particular, I would advise to look for a library implementing this protocol in your target language, which would save you some headaches :slight_smile:

2 Likes

Hi @zwifi, thanks again. I think I have made a mistake in my previous reply stating the Token request as the Authorisation request. Sorry for that. So here’s the complete flow that I use.

1. Dynamic client registration

final regResponse = await http.post(Uri.parse(regEndpoint),
    headers: <String, String>{
      'Accept': '*/*',
      'Content-Type': 'application/json',
      'Connection': 'keep-alive',
      'Accept-Encoding': 'gzip, deflate, br',
    },
    body: json.encode({
      'application_type': 'web',
      'scope': 'openid profile offline_access',
      'grant_types': ['authorization_code', 'refresh_token'],
      'redirect_uris': reidirUrlList,
      'token_endpoint_auth_method': 'client_secret_basic',
    }));

2. Authorisation request

final authResponse = await http.get(Uri.parse(authEndpoint),
    headers: <String, String>{
      'Accept': '*/*',
      'response_type': 'code',
      'scope': 'openid profile offline_access'
      'client_id': client.clientId,
      'redirect_uri'= 'uri',
      'code_challenge'_method': 'S256',
      'code_challenge': 'iyWwZ6lXXICIeBzwNYAap7...'
    });

3. Token request

var h = base64.encode('${client.clientId}:${client.clientSecret}'.codeUnits);
final tokenResponse = await http.post(client.issuer.tokenEndpoint,
    headers: <String, String> {
      'Accept': '*/*',
      'Accept-Encoding': 'gzip, deflate, br',
      'content-type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic $h',
    },
    body: json.encode({
      'grant_type': 'authorization_code',
      'code': code,
      'redirect_uri': redirectUri.toString(),
      'client_id': client.clientId,
      'code_verifier': _proofKeyForCodeExchange['code_verifier']
    }));

But I am still not getting the refresh_token in the token response. I only get the following for the token response.

{expires_at: 1710475763, 
access_token: eyJhbGciOiJ..., 
expires_in: 3600, 
id_token: eyJhbGciOiJFUzI1NiIsInR5c..., 
scope: *this field is empty for some reason*, 
token_type: Bearer}
2 Likes

Thanks for the detailed flow, that’s very thorough. Unfortunately, nothing jumps at me from an OpenID point of view, all looks correct to me from a protocol perspective. When you log in, you should get some kind of consent prompt asking you to authorize our client, does it mention any scopes?

In any case, I’m afraid I won’t be able to help. If there aren’t other responses here, maybe you could open an issue on the CSS GitHub repository, you may have some configuration issue, as you mentioned initially.

2 Likes

node-oidc-provider, which CSS uses, is pretty strict.
I don’t think you’ve shown here what prompt is being specified - maybe you’re running into this issue?

Unfortunately the problem you’re running into seems like it would require debugging of node-oidc-provider within CSS, which is a bit tricky.

2 Likes

Hi @zwifi and @josephguillaume,

Thanks a lot for your responses with clear explanations.

The change suggested by @josephguillaume worked finally. I wasn’t specifying the prompt specifically in my requests earlier. Thanks for pointing that out.

So for anyone who is following the thread here’s the final flow of requests that I am using now to get the response with a refresh token.

1. Dynamic client registration

final regResponse = await http.post(Uri.parse(regEndpoint),
    headers: <String, String>{
      'Accept': '*/*',
      'Content-Type': 'application/json',
      'Connection': 'keep-alive',
      'Accept-Encoding': 'gzip, deflate, br',
    },
    body: json.encode({
      'application_type': 'web',
      'scope': 'openid profile offline_access',
      'grant_types': ['authorization_code', 'refresh_token'],
      'redirect_uris': reidirUrlList,
      'token_endpoint_auth_method': 'client_secret_basic',
    }));

2. Authorisation request

final authResponse = await http.get(Uri.parse(authEndpoint),
    headers: <String, String>{
      'Accept': '*/*',
      'response_type': 'code',
      'scope': 'openid profile offline_access'
      'client_id': client.clientId,
      'redirect_uri': 'uri',
      'prompt': 'consent',
      'state': '8LZkwOr...'
      'code_challenge_method': 'S256',
      'code_challenge': 'iyWwZ6lXXICIeBzwNYAap7...'
    });

3. Token request

var h = base64.encode('${client.clientId}:${client.clientSecret}'.codeUnits);
final tokenResponse = await http.post(client.issuer.tokenEndpoint,
    headers: <String, String> {
      'Accept': '*/*',
      'Accept-Encoding': 'gzip, deflate, br',
      'content-type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic $h',
    },
    body: json.encode({
      'grant_type': 'authorization_code',
      'code': code,
      'redirect_uri': redirectUri.toString(),
      'client_id': client.clientId,
      'code_verifier': _proofKeyForCodeExchange['code_verifier']
    }));

With this the server responds with all three tokens, access_token, id_token, and refresh_token.

4 Likes