* This function encodes the OAuth state by generating a random SID, storing it * in a cookie, and returning a JSON string containing the original state and * the SID. The cookie is used to validate the authenticity of the callback * request later. * * This mechanism allows to bind a particular
(event: H3Event, data: OAuthStateData)
| 141 | * @returns A JSON string encapsulating the original state and the generated SID |
| 142 | */ |
| 143 | function encodeOAuthState(event: H3Event, data: OAuthStateData): string { |
| 144 | const id = generateRandomHexString() |
| 145 | // This uses an ephemeral cookie instead of useSession() to avoid polluting |
| 146 | // the session with ephemeral OAuth-specific data. The cookie is set with a |
| 147 | // short expiration time to limit the window of potential misuse, and is |
| 148 | // deleted immediately after validating the callback to clean up any remnants |
| 149 | // of the authentication flow. Using useSession() for this would require |
| 150 | // additional logic to clean up the session in case of expired ephemeral data. |
| 151 | |
| 152 | // We use the id as cookie name to allow multiple concurrent auth flows (e.g. |
| 153 | // user opens multiple tabs and initiates auth in both, or initiates auth, |
| 154 | // waits for a while, then initiates again before completing the first one), |
| 155 | // without risk of cookie value collisions between them. The cookie value is a |
| 156 | // constant since the actual value doesn't matter - it's just used as a flag |
| 157 | // to validate the presence of the cookie on callback. |
| 158 | setCookie(event, `${OAUTH_REQUEST_COOKIE_PREFIX}_${id}`, '1', { |
| 159 | maxAge: 60 * 5, |
| 160 | httpOnly: true, |
| 161 | // secure only if NOT in dev mode |
| 162 | secure: !import.meta.dev, |
| 163 | sameSite: 'lax', |
| 164 | path: event.path.split('?', 1)[0], |
| 165 | }) |
| 166 | |
| 167 | return JSON.stringify({ data, id }) |
| 168 | } |
| 169 | |
| 170 | function generateRandomHexString(byteLength: number = 16): string { |
| 171 | return Array.from(crypto.getRandomValues(new Uint8Array(byteLength)), byte => |
no test coverage detected