| 1 |
# OAuth2 PKCE |
| 2 |
|
| 3 |
Makenot.work supports OAuth2 Authorization Code with PKCE for "Log in with Makenot.work" flows. This lets third-party applications authenticate users without handling their passwords directly. |
| 4 |
|
| 5 |
## Overview |
| 6 |
|
| 7 |
1. Your app generates a PKCE code verifier and challenge |
| 8 |
2. User is redirected to `makenot.work/oauth/authorize` to log in and consent |
| 9 |
3. Makenot.work redirects back with an authorization code |
| 10 |
4. Your app exchanges the code for a JWT access token |
| 11 |
5. Use the token to call SyncKit or userinfo endpoints |
| 12 |
|
| 13 |
## Client Registration |
| 14 |
|
| 15 |
Your OAuth client ID is the API key of your SyncKit app. Create a SyncKit app from the Makenot.work dashboard to get one. |
| 16 |
|
| 17 |
### Redirect URIs |
| 18 |
|
| 19 |
**Localhost**: `http://127.0.0.1:{port}/...` and `http://localhost:{port}/...` are always allowed without registration. Use these for desktop apps. |
| 20 |
|
| 21 |
**Remote**: Non-localhost redirect URIs must be registered on your SyncKit app. Email info@makenot.work to add them. |
| 22 |
|
| 23 |
## Authorization Request |
| 24 |
|
| 25 |
Redirect the user to the authorize endpoint: |
| 26 |
|
| 27 |
``` |
| 28 |
GET /oauth/authorize |
| 29 |
?response_type=code |
| 30 |
&client_id=<your-api-key> |
| 31 |
&redirect_uri=http://127.0.0.1:8765/callback |
| 32 |
&state=<random-string> |
| 33 |
&code_challenge=<S256-challenge> |
| 34 |
&code_challenge_method=S256 |
| 35 |
``` |
| 36 |
|
| 37 |
|
| 38 |
|
| 39 |
| `response_type` | Yes | Must be `code` | |
| 40 |
| `client_id` | Yes | Your SyncKit app API key | |
| 41 |
| `redirect_uri` | Yes | Where to send the authorization code | |
| 42 |
| `state` | Yes | Random string to prevent CSRF; verify it in the callback | |
| 43 |
| `code_challenge` | Yes | Base64url-encoded SHA-256 hash of the code verifier | |
| 44 |
| `code_challenge_method` | Yes | Must be `S256` | |
| 45 |
|
| 46 |
The user sees a consent page. After logging in and approving, they are redirected to: |
| 47 |
|
| 48 |
``` |
| 49 |
{redirect_uri}?code=<authorization-code>&state=<your-state> |
| 50 |
``` |
| 51 |
|
| 52 |
## Token Exchange |
| 53 |
|
| 54 |
Exchange the authorization code for an access token: |
| 55 |
|
| 56 |
``` |
| 57 |
POST /oauth/token |
| 58 |
Content-Type: application/json |
| 59 |
|
| 60 |
{ |
| 61 |
"grant_type": "authorization_code", |
| 62 |
"code": "<authorization-code>", |
| 63 |
"redirect_uri": "http://127.0.0.1:8765/callback", |
| 64 |
"code_verifier": "<original-code-verifier>", |
| 65 |
"client_id": "<your-api-key>" |
| 66 |
} |
| 67 |
``` |
| 68 |
|
| 69 |
Response: |
| 70 |
|
| 71 |
```json |
| 72 |
{ |
| 73 |
"access_token": "eyJ...", |
| 74 |
"token_type": "Bearer", |
| 75 |
"expires_in": 604800, |
| 76 |
"user_id": "550e8400-...", |
| 77 |
"app_id": "660f9500-..." |
| 78 |
} |
| 79 |
``` |
| 80 |
|
| 81 |
The authorization code is single-use. The server verifies `SHA256(code_verifier) == code_challenge` before issuing a token. |
| 82 |
|
| 83 |
## User Info |
| 84 |
|
| 85 |
Retrieve the authenticated user's profile: |
| 86 |
|
| 87 |
``` |
| 88 |
GET /oauth/userinfo |
| 89 |
Authorization: Bearer <access_token> |
| 90 |
``` |
| 91 |
|
| 92 |
Response: |
| 93 |
|
| 94 |
```json |
| 95 |
{ |
| 96 |
"user_id": "550e8400-...", |
| 97 |
"username": "alice", |
| 98 |
"display_name": "Alice", |
| 99 |
"avatar_url": "https://makenot.work/static/avatars/alice.jpg" |
| 100 |
} |
| 101 |
``` |
| 102 |
|
| 103 |
## PKCE Implementation |
| 104 |
|
| 105 |
PKCE prevents authorization code interception: |
| 106 |
|
| 107 |
1. Generate a random code verifier (43-128 characters, URL-safe) |
| 108 |
2. Compute `code_challenge = BASE64URL(SHA256(code_verifier))` |
| 109 |
3. Send `code_challenge` in the authorization request |
| 110 |
4. Send `code_verifier` in the token exchange |
| 111 |
|
| 112 |
The server rejects token requests where the verifier does not match the challenge. |
| 113 |
|
| 114 |
### Example (Rust) |
| 115 |
|
| 116 |
```rust |
| 117 |
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; |
| 118 |
use sha2::{Digest, Sha256}; |
| 119 |
|
| 120 |
let verifier: String = (0..64) |
| 121 |
.map(|_| rand::random::<u8>()) |
| 122 |
.map(|b| format!("{:02x}", b)) |
| 123 |
.collect(); |
| 124 |
|
| 125 |
let challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes())); |
| 126 |
``` |
| 127 |
|
| 128 |
## Token Usage |
| 129 |
|
| 130 |
The access token works with all SyncKit endpoints: |
| 131 |
|
| 132 |
- [Cloud Sync](./synckit.md): push/pull data, manage devices |
| 133 |
- [OTA Updates](./ota.md): manage releases and artifacts |
| 134 |
- User info (above) |
| 135 |
|
| 136 |
Tokens expire after 7 days. After expiration, redirect the user through the authorization flow again. |
| 137 |
|
| 138 |
## Error Handling |
| 139 |
|
| 140 |
Authorization errors redirect to `redirect_uri` with an `error` parameter: |
| 141 |
|
| 142 |
``` |
| 143 |
{redirect_uri}?error=access_denied&state=<your-state> |
| 144 |
``` |
| 145 |
|
| 146 |
Token exchange errors return JSON: |
| 147 |
|
| 148 |
```json |
| 149 |
{ |
| 150 |
"error": "invalid_grant" |
| 151 |
} |
| 152 |
``` |
| 153 |
|
| 154 |
|
| 155 |
|
| 156 |
| `access_denied` | User denied consent | |
| 157 |
| `invalid_client` | Unknown client_id | |
| 158 |
| `invalid_grant` | Code expired, already used, or verifier mismatch | |
| 159 |
| `invalid_request` | Missing required parameters | |
| 160 |
|
| 161 |
## See Also |
| 162 |
|
| 163 |
- [API Overview](./api-overview.md): authentication methods and rate limits |
| 164 |
- [SyncKit Cloud Sync](./synckit.md): using the token for data sync |
| 165 |
|