-
Notifications
You must be signed in to change notification settings - Fork 10
docs: add PKCE OAuth2 guide for public clients #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,142 @@ Before integrating with Blink OAuth2, it's crucial to have a foundational unders | |||||||||||||
| The connecting applications are using the [OAuth2 authorization code flow](https://www.ory.sh/docs/oauth2-oidc/authorization-code-flow).<br /> | ||||||||||||||
| For a hands-on introduction to setting up OAuth2 with Ory Hydra, you can explore this [5-minute tutorial](https://www.ory.sh/docs/hydra/5min-tutorial). | ||||||||||||||
|
|
||||||||||||||
| ## Public Clients (PKCE) | ||||||||||||||
|
|
||||||||||||||
| ### When to Use PKCE | ||||||||||||||
|
|
||||||||||||||
| PKCE (Proof Key for Code Exchange) is designed for **public clients** that cannot securely store a client secret. This includes: | ||||||||||||||
|
|
||||||||||||||
| - **Mobile applications** (iOS, Android, React Native) | ||||||||||||||
| - **Single Page Applications (SPAs)** running in a browser | ||||||||||||||
| - **CLI tools** and desktop applications | ||||||||||||||
| - **Any client-side application** where the source code is accessible to users | ||||||||||||||
|
|
||||||||||||||
| PKCE uses the OAuth2 Authorization Code flow enhanced with [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) to provide security without requiring a client secret. | ||||||||||||||
|
|
||||||||||||||
| ### How PKCE Works | ||||||||||||||
|
|
||||||||||||||
| PKCE replaces the client secret with a dynamically generated proof key: | ||||||||||||||
|
|
||||||||||||||
| 1. **Client generates** a random `code_verifier` (43-128 characters, URL-safe) | ||||||||||||||
| 2. **Client creates** `code_challenge` = base64url(SHA256(code_verifier)) | ||||||||||||||
| 3. **Authorization request** includes `code_challenge` and `code_challenge_method=S256` | ||||||||||||||
| 4. **Token exchange** sends `code_verifier` instead of `client_secret` | ||||||||||||||
| 5. **Server verifies** the hash matches — proving the same client that started the flow is completing it | ||||||||||||||
|
|
||||||||||||||
| This prevents authorization code interception attacks since an attacker would need both the authorization code and the original `code_verifier`. | ||||||||||||||
|
|
||||||||||||||
| ### Register as a Public Client | ||||||||||||||
|
|
||||||||||||||
| To use PKCE with Blink: | ||||||||||||||
|
|
||||||||||||||
| 1. Contact the Blink development team via [chat.blink.sv](https://chat.blink.sv) for application approval | ||||||||||||||
| 2. **Specify** that you need a **public client** registration (no client secret) | ||||||||||||||
| 3. **Provide** your callback URL — for mobile apps, this is typically: | ||||||||||||||
| - A deep link scheme: `cypherbox://auth/callback` | ||||||||||||||
| - A universal link: `https://cypherbox.app/auth/callback` | ||||||||||||||
| - For SPAs: your domain with HTTPS: `https://myapp.com/callback` | ||||||||||||||
|
|
||||||||||||||
| After approval, you'll receive only a **Client ID** (no secret). | ||||||||||||||
|
|
||||||||||||||
| ### The PKCE OAuth2 Flow | ||||||||||||||
|
|
||||||||||||||
| #### Step 1: Generate Code Verifier and Challenge | ||||||||||||||
|
|
||||||||||||||
| ```bash | ||||||||||||||
| # Generate a random code_verifier (43-128 characters) | ||||||||||||||
| CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-') | ||||||||||||||
|
|
||||||||||||||
| # Create the code_challenge (SHA256 hash, base64url-encoded) | ||||||||||||||
| CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '/+' '_-') | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### Step 2: Build the Authorization URL | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| https://oauth.blink.sv/oauth2/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=cypherbox%3A%2F%2Fauth%2Fcallback&scope=read+receive&state=RANDOM_STATE&code_challenge=CODE_CHALLENGE&code_challenge_method=S256 | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| **Key differences from confidential clients:** | ||||||||||||||
| - Includes `code_challenge` and `code_challenge_method=S256` | ||||||||||||||
| - No `client_secret` in any step | ||||||||||||||
|
|
||||||||||||||
| #### Step 3: User Authentication | ||||||||||||||
|
|
||||||||||||||
| The user authenticates and approves your application. Blink redirects to your callback URL with the authorization code: | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| cypherbox://auth/callback?code=ory_ac_AUTHORIZATION_CODE&scope=read+receive&state=RANDOM_STATE | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### Step 4: Exchange Code for Token | ||||||||||||||
|
|
||||||||||||||
| **Critical:** Send `code_verifier` instead of `client_secret`: | ||||||||||||||
|
|
||||||||||||||
| ```bash | ||||||||||||||
| curl -X POST https://oauth.blink.sv/oauth2/token \ | ||||||||||||||
| -d "grant_type=authorization_code" \ | ||||||||||||||
| -d "code=$AUTHORIZATION_CODE" \ | ||||||||||||||
| -d "redirect_uri=cypherbox://auth/callback" \ | ||||||||||||||
| -d "client_id=$YOUR_CLIENT_ID" \ | ||||||||||||||
| -d "code_verifier=$CODE_VERIFIER" | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### Step 5: Handle the Access Token | ||||||||||||||
|
|
||||||||||||||
| The response format is identical to confidential clients: | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| { | ||||||||||||||
| "access_token": "ory_at_...", | ||||||||||||||
| "expires_in": 3599, | ||||||||||||||
| "scope": "read receive", | ||||||||||||||
| "token_type": "bearer" | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Mobile App Implementation Notes | ||||||||||||||
|
|
||||||||||||||
| #### Deep Links and Universal Links | ||||||||||||||
| - **Deep links** (`cypherbox://auth/callback`) work on all platforms but may prompt the user to open your app | ||||||||||||||
| - **Universal links** (`https://cypherbox.app/auth/callback`) provide a smoother experience on iOS | ||||||||||||||
| - The OS automatically routes the redirect back to your app — no backend server needed | ||||||||||||||
|
|
||||||||||||||
| #### Secure Token Storage | ||||||||||||||
| - **iOS:** Store tokens in the iOS Keychain using `keychain-services` | ||||||||||||||
|
||||||||||||||
| - **iOS:** Store tokens in the iOS Keychain using `keychain-services` | |
| - **iOS:** Store tokens in the iOS Keychain (Keychain Services) |
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, the refresh token curl example uses raw -d fields. Using --data-urlencode (especially for refresh_token, which can include characters needing encoding) will make the example more robust across shells and clients.
| -d "grant_type=refresh_token" \ | |
| -d "refresh_token=$REFRESH_TOKEN" \ | |
| -d "client_id=$YOUR_CLIENT_ID" | |
| --data-urlencode "grant_type=refresh_token" \ | |
| --data-urlencode "refresh_token=$REFRESH_TOKEN" \ | |
| --data-urlencode "client_id=$YOUR_CLIENT_ID" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The curl example uses
-d "redirect_uri=cypherbox://auth/callback"(and other params) as a raw form field. For strictapplication/x-www-form-urlencodedcompliance and to avoid subtle encoding issues with redirect URIs/verifiers, use--data-urlencodefor fields likeredirect_uri(and ideallycode_verifier).