Authorization code grant with PKCE (Proof Key for Code Exchange)
This flow is good for applications that cannot securely store a client_secret, such as single-page applications (SPAs), mobile apps, and desktop applications. We require PKCE to enhance the security of the authorization code flow by adding an additional verification step during the token exchange process.
Use Cases:
- Native Mobile Applications: Clients (iOS, Android apps) that cannot securely store a traditional, static client_secret within their distributed code.
- Single-Page Applications (SPAs): Client-side JavaScript applications running in a browser that are vulnerable to code inspection, making them unable to secure a static client_secret.
- Desktop Applications: Applications running on an end-user's machine, which are also considered public clients for security purposes.
- Confidential Clients (Web Apps with a Backend): Although they can use a client_secret, PKCE is still recommended as an extra layer of defense against certain types of code injection and CSRF attacks.
Key Benefits:
- Eliminates Client Secret for Public Clients: Authorization code flow with PKCE allows public clients (like SPAs and mobile apps) to securely use the authorization code grant without needing to store a client_secret, which they cannot do safely.
- Mitigates Cross-Site Request Forgery (CSRF): By dynamically linking the initial authorization request to the final token exchange, PKCE provides strong protection against CSRF, often making the use of the state parameter redundant for this specific purpose.
- Default Security Standard: PKCE is now mandated in the OAuth 2.0 Security Best Current Practice and the upcoming OAuth 2.1 specification, making it the required flow for modern application development.
Authorization Code Flow with PKCE
Flow Steps in Detail
Step 1: Authorization Request
The client application redirects the user to the authorization endpoint with the following parameters:
Request:
GET /w/{tenant}/oauth/api/v1/authorize?
response_type=code&
client_id=your-client-id&
redirect_uri=https://yourapp.com/callback&
scope=openid+profile+email+patient/*.read&
state=random-state-value&
code_challenge=CHALLENGE&
code_challenge_method=S256 HTTP/1.1
Host: api.haste.health
Parameters:
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code for authorization code flow |
client_id | Yes | Your application's client identifier |
redirect_uri | Yes | Where to redirect after authorization (must be pre-registered) |
scope | Yes | Space-delimited list of requested scopes (must include openid for OIDC) |
state | Recommended | Random value to prevent CSRF attacks |
code_challenge | Required | PKCE code challenge (SHA-256 hash of code_verifier) |
code_challenge_method | Required | Must be S256 (SHA-256 hash method) |
nonce | Optional | Random value included in ID token to prevent replay attacks |
prompt | Optional | none, login, consent, or select_account |
max_age | Optional | Maximum authentication age in seconds |
Step 2: User Authentication
The authorization server authenticates the user through:
- Login form (username/password, MFA)
- Session cookie (if already logged in)
- Federated identity provider (SSO)
Step 3: User Consent
If required, the authorization server displays a consent screen showing:
- Application name and description
- Requested scopes and permissions
- Option to approve or deny
Step 4: Authorization Code Generation
Upon successful authentication and consent, the authorization server:
- Generates a short-lived authorization code (typically 1-10 minutes)
- Associates the code with the client, user, and requested scopes
- Redirects back to the client's redirect URI
Response:
HTTP/1.1 302 Found
Location: https://yourapp.com/callback?
code=AUTH_CODE_HERE&
state=random-state-value
Step 5: Token Exchange
The SPA exchanges the authorization code for tokens using the code_verifier (no client_secret needed for public clients):
Request:
POST /w/{tenant}/oauth/api/v1/token HTTP/1.1
Host: api.haste.health
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=https://yourapp.com/callback&
client_id=spa-client-id&
code_verifier=ORIGINAL_VERIFIER
Parameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
code | Yes | The authorization code received from callback |
redirect_uri | Yes | Must exactly match the original redirect URI |
client_id | Yes | Your SPA's registered client identifier |
code_verifier | Required | The original random string used to generate code_challenge (retrieved from sessionStorage) |
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email patient/*.read",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Step 6: Access Protected Resources
Use the access token to call protected APIs:
Request:
GET /w/{tenant}/{project}/api/v1/fhir/r4/Patient/123 HTTP/1.1
Host: api.haste.health
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Error Handling
Authorization Errors
| Error Code | Description | Resolution |
|---|---|---|
invalid_request | Missing or invalid required parameter | Check all required parameters are included |
unauthorized_client | Client not authorized for this grant type | Verify client configuration |
access_denied | User denied authorization request | Inform user and allow retry |
unsupported_response_type | Authorization server doesn't support response type | Use response_type=code |
invalid_scope | Requested scope is invalid or unknown | Check scope names and permissions |
server_error | Authorization server error | Retry with exponential backoff |
temporarily_unavailable | Server temporarily unavailable | Retry after delay |
Token Exchange Errors
| Error Code | Description | Resolution |
|---|---|---|
invalid_request | Missing required parameter | Include all required parameters |
invalid_client | Client authentication failed | Verify client_id and client_secret |
invalid_grant | Authorization code invalid, expired, or revoked | Request new authorization |
unauthorized_client | Client not authorized for authorization code grant | Update client configuration |
unsupported_grant_type | Grant type not supported | Use grant_type=authorization_code |
Example Error Response
{
"error": "invalid_grant",
"error_description": "The authorization code has expired or has already been used",
"error_uri": "https://api.haste.health/errors/invalid_grant"
}
Comparison with Other Flows
| Feature | Authorization Code + PKCE | Client Credentials | Implicit (Deprecated) |
|---|---|---|---|
| Use Case | SPAs, mobile apps | Service-to-service | SPAs (legacy) |
| Client Secret | Not required (PKCE) | Required | Not possible |
| Refresh Tokens | Yes | Optional | No |
| Security | Highest | High | Low |
| User Context | Yes | No | Yes |
| PKCE Required | ✅ Yes | ❌ No | N/A |
| Recommended | ✅ Yes | ✅ Yes | ❌ No (use Auth Code + PKCE) |