How to fake the Authorization Code Flow with PKCE for system tests
The username and password are never saved in the backend during the login operation. The identity provider will remember the user via cookies set after successful login after step 2. The relevant requests to be observed in the browser are to /auth, /authenticate and /token during the OAuth 2.0 Authorization Code Grant Flow with PKCE.
More information about the flow is available at RFC 7636 section 1.1
The URLs contain placeholder in the form of <foo> to be replaced to make them work.
|
For security, it is recommended to randomize and compute the nonce, code challenge etc. dynamically. |
The flow is as follows:
-
User navigates to the application in the browser, the application notices that the user is not logged in yet and redirects them to the login form of the identity provider.
The identity provider can be Multitenant Access Control itself or any other OIDC identity provider.
Request:
GET /iam/auth/realms/<tenant-id>/protocol/openid-connect/auth?client_id=<client-id>&redirect_uri=<redirect-url>&state=c69e005f-2737-43d4-aeca-bbfb76ed5296&response_mode=fragment&response_type=code&scope=openid&nonce=7b1c94a1-65b4-4f73-934a-c7991fce8a0c&code_challenge=OVC-YF30UGCsIMzyRAiW3zzk2lxrZdLiyeQ-P6JGDZQ&code_challenge_method=S256
This URL contains the redirect_uri which tell the identity provider where the user should be redirected to after successful login. -
On /auth the user is shown the login form of the identity provider - or redirected back the application if the user is already logged in (active “single sign on” (SSO) session)
The user will enter the username and password here and submit the form, in case of Multitenant Access Control as identity provider that is the /authenticate path.
Request (using the URL of the form’s action attribute so send form parameters):
POST /iam/auth/realms/<tenant-id>/login-actions/authenticate?session_code=A1Fy8TTAa2wOjxQo5icGy80KBc5GVmPhc0B7E7DiU9Q&execution=f23aa7c3-ae77-47c6-b882-16d9f41bc394&client_id=<client-id>&tab_id=5SS7TIvbXRI
with form parametersusername=<username-here>&password=<password-here>&credentialId=
-
If the login was successful, the response to the POST-request will have the status code 302 temporary redirect and a Location header
Location/?tenant=<tenant-id>#state=c69e005f-2737-43d4-aeca-bbfb76ed5296&session_state=7882ff23-db6d-4c90-82e4-ceca208185a2&code=<authorization-code-from-auth-server>
This redirect URL will match the query parameter from step 1. and bring the user back to the application.
The most relevant parameter here is thecode
query parameter, containing the authorization code that will be used to get a token for the user. In the past when using the implicit flow you’d get the token here directly instead of the code, but as it was part of the URL it could easily be intercepted by evil apps, request logs and so on. -
In the last request the code is exchanged for a token.
Request:POST /iam/auth/realms/<tenant-id>/protocol/openid-connect/token
with form parameterscode=<authorization-code-from-location-header>&grant_type=authorization_code&client_id=<client-id>&redirect_uri=<redirect-url>&code_verifier=Yp8FDXK5EL4bubJdOPdAaaVSuO9TMGkEkvyo11haEeHwvRGEzxEKfQqMZp9wscbrkR3AY7DsFfIlVoeSq9HIIfrN9SQebRju
As you can see, this request contains the “code” from step 3., as well as the code_verifier that must be hashed with the code_challenge_method from step 1. and should result in the value of code_challenge of step 1.
Code_verifier, code_challenge and code_challenge_method are part of the PKCE (proof of key for code exchange) to avoid interception / misuse of the “code” from step 3. The response will contain an access_token, refresh_token and id_token for the user and expiry timestamps for those tokens.
The redirect_uri must equal the one redirect_uri used in step 1. |
Hints for implementing those flows manually without UI interaction:
-
If you use a confidential client the code_challenge, code_challenge_method and the code_verifier are not needed, instead a client_secret must be sent along with the request to the /token endpoint when exchanging the code for a token.
-
The code_verifier, code_challenge and code_challenge_method can be reused from a logged / observed login flow, they just have to match. In our implementation we therefore just used hardcoded once from an observed flow, but they could be computed dynamically as well without too much hassle.
-
You’ll find the client_id in most of the URLs above, just replace its value with your client_id of the public client – e.g. the Portal’s frontend client. You also have to replace the tenant-Id with the one you want to use in the paths and obviously the domain.
-
As redirect URL you can typically just use the url of the portal.
-
The URL for the login form POST in step 2 can be extracted from the form received in the response of step 1. As for any regular html form, it’s the action “attribute” of the “form”.
-
You have to read the “code” query parameter from the Location header of the response from the login form POST of step 2.
-
Remember to add the cookies received with the response form the /auth request in step 1 to the login form POST in step 2 if your framework does not do that automatically (Gatling and I believe also Cypress do so).