# Exploring the OpenID Password Grant Refresh in Several Popular OIDC Providers ## Okta ### IDP Configuration - The OIDC client must be configured to allow password grant bt creating a "Native App" in the Okta UI. - Additionally, it must be configured to allow refresh by checking the "Refresh Token" box on the App's configuration. - There is another option on the App's configuration to either "Rotate token after every use" or "Use persistent token" (the default). - Refresh token lifetime is configurable in the "Authorization Server" settings, but not on the client itself. ### Perform an Authorization ```bash curl -X POST https://dev-475662.okta.com/oauth2/default/v1/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=password&username=walrus@example.com&password=XXX&scope=openid%20profile%20email%20offline_access&client_id=XXX&client_secret=XXX' ``` The response includes: access token, ID token, and refresh token. If the request does not contain the `offline_access` scope then the response will not contain a refresh token. Note that the discovery document did include `offline_access` in the `scopes_supported` array. ### Perform a Refresh ```bash curl -X POST https://dev-475662.okta.com/oauth2/default/v1/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token&client_id=XXX&client_secret=XXX&refresh_token=XXX' ``` The response includes: a new access token, the same scopes from the original authorization request, and a new ID token. The same refresh token is returned again for the "Use persistent token" App configuration. A new refresh token is returned for the "Rotate token after every use" App configuration. ### Discovering the Revocation Endpoint ```bash curl https://dev-475662.okta.com/oauth2/default/.well-known/openid-configuration ``` The result includes, among other keys, those documented by [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2): ```json { "revocation_endpoint": "https://dev-475662.okta.com/oauth2/default/v1/revoke", "revocation_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none" ] } ``` ### Revoking the Refresh Token Formulate a request to the revoke endpoint according to [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009#section-2.1). ```bash curl -X POST https://dev-475662.okta.com/oauth2/default/v1/revoke \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'client_id=XXX&client_secret=XXX&token_type_hint=refresh_token&token=XXX' ``` The response is a `200 OK` with no body. After this request, using the refresh token to perform another refresh fails, as expected. ## Gitlab ### IDP Configuration Create a confidential application, check "expire access tokens" and the scopes openid, profile and email. Copy the client id and secret. ### Perform an Authorization Following their docs, ```bash $ echo 'scope=openid%20profile%20email%20offline_access&grant_type=password&username=vmware-margaretc&password=xyz' > auth.txt $ curl --data "@auth.txt" --user client_id:client_secret --request POST "https://gitlab.com/oauth/token" {"error":"invalid_scope","error_description":"The requested scope is invalid, unknown, or malformed."} ``` If we *don't* ask for `scope=offline_access`, it works and it gives us a refresh token even though we didn't ask for it: ```bash $ echo 'scope=openid%20profile%20email&grant_type=password&username=vmware-margaretc&password=xyz' > auth.txt $ curl --data "@auth.txt" --user client_id:client_secret --request POST "https://gitlab.com/oauth/token" ``` So, Okta requires offline_access to get a refresh token, and Gitlab prohibits it. Note that the discovery document did not include `offline_access` listed in `scopes_supported`. Maybe Pinniped could look at that to decide not to send `offline_access` in the list of requested scopes? ### Perform a Refresh ```bash curl -X POST https://gitlab.com/oauth/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token&client_id=XXX&client_secret=XXX&refresh_token=XXX' ``` The response includes: a new access token, the same scopes from the original authorization request, and a new ID token. A different refresh token is always returned. ### Discovering the Revocation Endpoint ```bash $ curl https://gitlab.com/.well-known/openid-configuration ``` The result include: ``` "revocation_endpoint": "https://gitlab.com/oauth/revoke", ``` ### Revoking the Refresh Token Formulate a request to the revoke endpoint according to [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009#section-2.1). ```bash curl -X POST https://gitlab.com/oauth/revoke \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'client_id=XXX&client_secret=XXX&token_type_hint=refresh_token&token=XXX' ``` The response is a `200 OK` with an empty json body. After this request, using the refresh token to perform another refresh fails, as expected. ## Azure AD ### IDP Configuration https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc To get your tenant ID, go to portal.azure.com > Azure Active Directory > Overview. To edit your client, go to portal.azure.com > Azure Active Directory > App registrations > Click on the client name. - On "Overview" you can copy the "Application (client) ID" value. - On "Overview" you can add a client secret using the "Add a certificate or secret" link. - In "Authentication" you can enable the password grant with the "Enable the following mobile and desktop flows" checkbox. - In "API Permissions", choose "Add a permission", thebn "Microsoft graph", then "Delegated permissions", then "OpenId permissions", then choose "email", "offline_access", "openid", and "profile", then click "Add permissions". - In "Token Configuration" optional claims can be added to the ID tokens, such as "email", etc. ### Perform an Authorization ```bash curl -X POST https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=password&username=username%40mycompany.com&password=XXX&scope=openid%20profile%20email%20offline_access&client_id=XXX&client_secret=XXX' ``` We were not able to get this to work, probably because of the way that our corporate Azure AD is configured. However, it is documented that it should work and that it should return a refresh token. Also according to the document, the `offline_access` scope should be requested. https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc Note that the discovery document did include `offline_access` in the `scopes_supported` array. ### Perform a Refresh Documened here: https://docs.microsoft.com/en-us/azure/active-directory/develop/refresh-tokens ### Discovering the Revocation Endpoint ```bash curl https://login.microsoftonline.com/TENANT_ID/v2.0/.well-known/openid-configuration ``` The result *does not* include `revocation_endpoint` or `revocation_endpoint_auth_methods_supported` as documented by [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2), or any other revocation-related keys. ### Revoking the Refresh Token Not applicable. There is no revocation endpoint. ## Dex Using Its Internal User Store This is used in Pinniped's CI for our integration tests. Note that we use a custom fork of Dex because we need [Mo's PR](https://github.com/dexidp/dex/pull/2234). ### IDP Configuration Our Dex configuration used in CI is here: https://github.com/vmware-tanzu/pinniped/blob/main/test/deploy/tools/dex.yaml ### Perform an Authorization ```bash curl -X POST https://dex.tools.svc.cluster.local/dex/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=password&username=pinny@example.com&password=XXX&scope=openid%20profile%20email%20offline_access&client_id=XXX&client_secret=XXX' ``` The response includes: access token, ID token, and refresh token. When `offline_access` isn't included, you don't get a refresh token. Note that the discovery document did include `offline_access` in the `scopes_supported` array. ### Perform a Refresh ```bash curl -X POST https://dex.tools.svc.cluster.local/dex/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token&client_id=XXX&client_secret=XXX&refresh_token=XXX' ``` The response includes a new refresh token, access token and id token. Reusing the old refresh token tells you that `{"error":"invalid_request","error_description":"Refresh token is invalid or has already been claimed by another client."}`, so the old one gets revoked. ### Discovering the Revocation Endpoint The discovery document doesn't specify `revocation_endpoint` or `revocation_endpoint_auth_methods_supported` ### Revoking the Refresh Token Not applicable. There is no revocation endpoint. ## VMWare Workspace One Identity Broker ### IDP Configuration The OIDC client can be created using a POST to the `/api/vcenter/identity/providers/oauth2clients` endpoint. It appears that the default for a client is to have the password grant and refresh grant allowed. Presumably these are the docs for the endpoint: https://developer.vmware.com/docs/vsphere-automation/latest/vcenter/api/vcenter/identity/providers/post/. ### Perform an Authorization ```bash curl -X POST https://sc2-XX-XX-242-157.eng.vmware.com/acs/t/CUSTOMER/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=password&username=walrus@example.com&password=XXX&scope=openid%20profile%20email%20offline_access&client_id=XXX&client_secret=XXX' ``` The response includes: access token, ID token, and refresh token. If the request does not contain the `offline_access` scope then the response still contains a refresh token anyway. Note that the discovery document have a `scopes_supported` array but it did not include `offline_access` in its list. The access token and ID token are both JWTs. The refresh token is opaque. The access token expires in 28798 (about 8 hours). The ID token expires in about an hour. Example ID token: ```json { "email": "federationuser@vmware.com", "exp": 1633066397, "iat": 1633037597, "sub": "federationuser@CUSTOMER", "iss": "https://sc2-XX-XX-242-157.eng.vmware.com/acs/t/CUSTOMER/", "aud": [ "WDujp7YJGLHdRHhBct23zwqxnfw6gLtOVuy5" ], "auth_time": 0, "azp": "WDujp7YJGLHdRHhBct23zwqxnfw6gLtOVuy5", "at_hash": "Jm00Vc_bWhMccitQckwNrg", "name": "Federation Test User", "given_name": "Federation Test", "family_name": "User", "email_verified": true, "updated_at": 0, "acct": "federationuser@XXX.eng.vmware.com", "ver": 1, "idp": "00o1yykvnnpwSv1gt5d6", "at_jti": "5c438c4c-1ff8-40ad-80d8-968e648ad810", "jti": "ID.JVYY-Sr55Q2osmjlceOtl95kOZvl4bu8lOpVv6rXgbc" } ``` ### Perform a Refresh ```bash curl -X POST --user 'clientID:clientSecret' https://sc2-XX-XX-242-157.eng.vmware.com/acs/t/CUSTOMER/token \ --header 'accept: application/json' --header 'content-type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token&refresh_token=XXX' ``` Note that strangely, even though the original password grant request allowed the client credentials to be part of the POST body, the refresh grant does not allow it. It returns the following error: ```json { "error": "invalid_client", "error_description": "oauth2.authorization.not.determined" } ``` However, sending the client credentials as a basic auth header works. The response includes a new access token and id token. The old refresh token is also returned. ### Discovering the Revocation Endpoint ```bash curl https://sc2-10-185-242-157.eng.vmware.com/acs/t/CUSTOMER/.well-known/openid-configuration ``` The result *does not* include `revocation_endpoint` or `revocation_endpoint_auth_methods_supported` as documented by [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2), or any other revocation-related keys. ### Revoking the Refresh Token Not applicable. There is no revocation endpoint.