speck OAuth2AuthorizationCode { // OAuth 2.0 Authorization Code Flow with PKCE // Implements RFC 6749 Section 4.1 (Authorization Code Grant) with RFC 7636 (PKCE) // Key security properties: CSRF protection, code reuse prevention, replay resistance, PKCE integrity // === Types === type ClientId = String type RedirectUri = String type State = String type CodeChallenge = String type CodeVerifier = String type AuthorizationCode = String type AccessToken = String type RefreshToken = String type Scope = String type TokenType = String type ClientSecret = String type UserId = String type SessionId = String type TokenResponse = { access_token: AccessToken, token_type: TokenType, expires_in: Nat, refresh_token: RefreshToken | null, scope: Scope } type AuthorizationRequest = { client_id: ClientId, redirect_uri: RedirectUri, response_type: String, // "code" state: State, code_challenge: CodeChallenge, code_challenge_method: String, // "S256" or "plain" scope: Scope } type TokenRequest = { client_id: ClientId, client_secret: ClientSecret, code: AuthorizationCode, redirect_uri: RedirectUri, code_verifier: CodeVerifier } type RefreshRequest = { client_id: ClientId, client_secret: ClientSecret, refresh_token: RefreshToken, scope: Scope | null } // === State === state { // Registered clients clients: Map(ClientId, { redirect_uris: Set(RedirectUri), client_secret: ClientSecret }) // Authorization codes in flight authorizationCodes: Map(AuthorizationCode, { client_id: ClientId, redirect_uri: RedirectUri, state: State, code_challenge: CodeChallenge, code_challenge_method: String, scope: Scope, user_id: UserId, session_id: SessionId, created_at: Nat, used: Bool }) // Access tokens issued accessTokens: Map(AccessToken, { client_id: ClientId, user_id: UserId, scope: Scope, expires_at: Nat, session_id: SessionId, refresh_token: RefreshToken }) // Refresh tokens refreshTokens: Map(RefreshToken, { client_id: ClientId, user_id: UserId, scope: Scope, access_token: AccessToken, created_at: Nat }) // User sessions sessions: Map(SessionId, { user_id: UserId }) // Nonce for state to prevent CSRF stateNonces: Map(State, { client_id: ClientId, redirect_uri: RedirectUri }) // PKCE challenge nonces pkceChallenges: Map(CodeChallenge, { code_verifier_hash: String, method: String }) // Clock (abstracted as monotonically increasing) currentTime: Nat } // === Initialization === init { clients == emptyMap authorizationCodes == emptyMap accessTokens == emptyMap refreshTokens == emptyMap sessions == emptyMap stateNonces == emptyMap pkceChallenges == emptyMap currentTime == 0 } // === Invariants === // Invariant 1: No token leakage - tokens are only accessible to authorized parties invariant NoTokenLeakage { // Access tokens are bound to specific clients and users forall token in accessTokens.keys: let t := accessTokens[token] t.client_id in clients.keys and exists s in sessions.keys: sessions[s].user_id == t.user_id // Refresh tokens are only accessible to the client that requested them forall rt in refreshTokens.keys: let r := refreshTokens[rt] r.client_id in clients.keys // Authorization codes are only sent to the registered redirect URI forall code in authorizationCodes.keys: let c := authorizationCodes[code] c.client_id in clients.keys and c.redirect_uri in clients[c.client_id].redirect_uris } // Invariant 2: CSRF protection - state parameter matches between authorize and callback invariant StateCSRFProtection { // Every state must have been registered by the client forall state in stateNonces.keys: let n := stateNonces[state] n.client_id in clients.keys and n.redirect_uri in clients[n.client_id].redirect_uris // State can only be used by the client that created it forall code in authorizationCodes.keys: let c := authorizationCodes[code] c.state in stateNonces.keys } // Invariant 3: Authorization codes are single-use (cannot be reused) invariant AuthorizationCodeSingleUse { forall code in authorizationCodes.keys: let c := authorizationCodes[code] c.used == false implies not exists action in next: action.name == "ExchangeCode" and action.params.code == code } // Invariant 4: Tokens have bounded validity (replay attack resistance) invariant TokenValidityBounded { // Access tokens expire after a reasonable time forall token in accessTokens.keys: let t := accessTokens[token] t.expires_at > currentTime // Refresh tokens expire after a longer period forall rt in refreshTokens.keys: let r := refreshTokens[rt] // (Simplified: refresh tokens should have expiration, but we track creation time) currentTime - r.created_at <= 7776000 // 90 days max } // Invariant 5: PKCE prevents authorization code interception invariant PKCECodeInterceptionPrevention { // Code verifier must hash to the code challenge forall challenge in pkceChallenges.keys: let c := pkceChallenges[challenge] // In practice, this would use actual hashing, but we verify the relationship exists verifier in authorizationCodes.keys: let a := authorizationCodes[verifier] a.code_challenge == challenge and a.code_challenge_method == c.method // Code verifier is only used once with the matching challenge forall code in authorizationCodes.keys: let c := authorizationCodes[code] c.used == false implies pkceChallenges[c.code_challenge].code_verifier_hash == hash(c.code_verifier, c.code_challenge_method) } // Invariant 6: Redirect URI must match between authorize and token exchange invariant RedirectUriConsistency { forall code in authorizationCodes.keys: let c := authorizationCodes[code] // The redirect URI used in token exchange must match what was in authorize true // This is enforced in the ExchangeCode action } // Invariant 7: Client authentication required for token exchange invariant ClientAuthenticationRequired { forall code in authorizationCodes.keys: let c := authorizationCodes[code] c.client_id in clients.keys implies exists r in refreshTokens.keys: refreshTokens[r].client_id == c.client_id } // === Actions === // Action 1: Authorize - Client requests authorization code action Authorize(client: ClientId, redirectUri: RedirectUri, scope: Scope, userId: UserId, sessionId: SessionId) { require client in clients.keys require redirectUri in clients.client.redirect_uris require sessionId in sessions.keys require sessions[sessionId].user_id == userId // Generate unique state (CSRF protection) let state := "state_" + hash(client + redirectUri + currentTime) // Generate PKCE code challenge let codeVerifier := "verifier_" + hash(client + userId + currentTime) let codeChallenge := hash(codeVerifier, "S256") // Register state nonce stateNonces[state] := { client_id: client, redirect_uri: redirectUri } // Register PKCE challenge pkceChallenges[codeChallenge] := { code_verifier_hash: hash(codeVerifier, "S256"), method: "S256" } // Create authorization code let authCode := "code_" + hash(client + userId + currentTime) authorizationCodes[authCode] := { client_id: client, redirect_uri: redirectUri, state: state, code_challenge: codeChallenge, code_challenge_method: "S256", scope: scope, user_id: userId, session_id: sessionId, created_at: currentTime, used: false } emit AuthorizationRequested { client_id: client, redirect_uri: redirectUri, state: state, scope: scope, user_id: userId, session_id: sessionId, authorization_code: authCode } } // Action 2: Approve - User grants consent (simulated: user confirms action) action ApproveAuthorization(code: AuthorizationCode, userId: UserId) { require code in authorizationCodes.keys let c := authorizationCodes[code] require c.user_id == userId require c.used == false // Mark as approved (ready for exchange) emit CodeIssued { authorization_code: code, client_id: c.client_id, redirect_uri: c.redirect_uri, state: c.state, scope: c.scope, user_id: userId } } // Action 3: ExchangeCode - Client exchanges code for tokens action ExchangeCode(req: TokenRequest) { require req.client_id in clients.keys // Verify authorization code exists and is not used require req.code in authorizationCodes.keys let c := authorizationCodes[req.code] require c.used == false require c.client_id == req.client_id require c.redirect_uri == req.redirect_uri // Verify PKCE: code_verifier must hash to code_challenge let expectedChallenge := hash(req.code_verifier, c.code_challenge_method) require expectedChallenge == c.code_challenge // Mark code as used (single-use enforcement) authorizationCodes[req.code].used := true // Remove PKCE challenge (one-time use) pkceChallenges[c.code_challenge] := null // Remove state nonce (one-time use) stateNonces[c.state] := null // Issue access token let accessToken := "access_" + hash(req.client_id + c.user_id + currentTime) let refreshToken := "refresh_" + hash(req.client_id + c.user_id + currentTime) let expiresAt := currentTime + 3600 // 1 hour accessTokens[accessToken] := { client_id: req.client_id, user_id: c.user_id, scope: c.scope, expires_at: expiresAt, session_id: c.session_id, refresh_token: refreshToken } refreshTokens[refreshToken] := { client_id: req.client_id, user_id: c.user_id, scope: c.scope, access_token: accessToken, created_at: currentTime } emit TokenIssued { access_token: accessToken, token_type: "Bearer", expires_in: 3600, refresh_token: refreshToken, scope: c.scope, client_id: req.client_id, user_id: c.user_id } } // Action 4: RefreshToken - Client refreshes expired access token action RefreshToken(req: RefreshRequest) { require req.client_id in clients.keys // Verify refresh token exists require req.refresh_token in refreshTokens.keys let r := refreshTokens[req.refresh_token] require r.client_id == req.client_id // Mark old access token as revoked (implicit) accessTokens[r.access_token] := null // Issue new tokens let newAccessToken := "access_" + hash(req.client_id + r.user_id + currentTime) let newRefreshToken := "refresh_" + hash(req.client_id + r.user_id + currentTime) let expiresAt := currentTime + 3600 accessTokens[newAccessToken] := { client_id: req.client_id, user_id: r.user_id, scope: req.scope.or(r.scope), expires_at: expiresAt, session_id: r.access_token.session_id, refresh_token: newRefreshToken } refreshTokens[newRefreshToken] := { client_id: req.client_id, user_id: r.user_id, scope: req.scope.or(r.scope), access_token: newAccessToken, created_at: currentTime } // Remove old refresh token refreshTokens[req.refresh_token] := null emit TokenRefreshed { access_token: newAccessToken, token_type: "Bearer", expires_in: 3600, refresh_token: newRefreshToken, scope: req.scope.or(r.scope), client_id: req.client_id, user_id: r.user_id } } // Action 5: RevokeToken - Client revokes a token action RevokeToken(client: ClientId, token: String, tokenType: String) { require client in clients.keys if tokenType == "access" { require token in accessTokens.keys let t := accessTokens[token] require t.client_id == client accessTokens[token] := null // Also revoke associated refresh token refreshTokens[t.refresh_token] := null } else if tokenType == "refresh" { require token in refreshTokens.keys let r := refreshTokens[token] require r.client_id == client refreshTokens[token] := null accessTokens[r.access_token] := null } emit TokenRevoked { token: token, token_type: tokenType, client_id: client } } // Action 6: PKCE Verification Step (explicit verification for documentation) action VerifyPKCE(code: AuthorizationCode, verifier: CodeVerifier) { require code in authorizationCodes.keys let c := authorizationCodes[code] require c.used == false // Verify the code verifier hashes to the code challenge let expectedChallenge := hash(verifier, c.code_challenge_method) require expectedChallenge == c.code_challenge // This action documents the PKCE verification step emit PKCEVerified { authorization_code: code, code_challenge: c.code_challenge, code_challenge_method: c.code_challenge_method, verifier_hash: hash(verifier, c.code_challenge_method) } } // === Next Actions === next: Authorize | ApproveAuthorization | ExchangeCode | RefreshToken | RevokeToken | VerifyPKCE // === Verification === verify: Always(NoTokenLeakage) verify: Always(StateCSRFProtection) verify: Always(AuthorizationCodeSingleUse) verify: Always(TokenValidityBounded) verify: Always(PKCECodeInterceptionPrevention) verify: Always(RedirectUriConsistency) verify: Always(ClientAuthenticationRequired) }