authenticate is the shared authentication logic that validates JWT tokens. Returns the user and claims, or an error. This is the single source of truth for token validation.
(ctx context.Context, accessTokenStr string)
| 165 | // authenticate is the shared authentication logic that validates JWT tokens. |
| 166 | // Returns the user and claims, or an error. This is the single source of truth for token validation. |
| 167 | func (in *APIAuthInterceptor) authenticate(ctx context.Context, accessTokenStr string) (*store.UserMessage, *claimsMessage, error) { |
| 168 | if accessTokenStr == "" { |
| 169 | return nil, nil, errs.New("access token not found") |
| 170 | } |
| 171 | |
| 172 | claims := &claimsMessage{} |
| 173 | if _, err := jwt.ParseWithClaims(accessTokenStr, claims, func(t *jwt.Token) (any, error) { |
| 174 | if t.Method.Alg() != jwt.SigningMethodHS256.Name { |
| 175 | return nil, errs.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256) |
| 176 | } |
| 177 | if kid, ok := t.Header["kid"].(string); ok { |
| 178 | if kid == "v1" { |
| 179 | return []byte(in.secret), nil |
| 180 | } |
| 181 | } |
| 182 | return nil, errs.Errorf("unexpected access token kid=%v", t.Header["kid"]) |
| 183 | }); err != nil { |
| 184 | if errors.Is(err, jwt.ErrTokenExpired) { |
| 185 | return nil, nil, errs.New("access token expired") |
| 186 | } |
| 187 | return nil, nil, errs.New("failed to parse claim") |
| 188 | } |
| 189 | |
| 190 | // Accept both user access tokens (bb.user.access) and OAuth2 access tokens (bb.oauth2.access) |
| 191 | if !audienceContains(claims.Audience, AccessTokenAudience) && !audienceContains(claims.Audience, OAuth2AccessTokenAudience) { |
| 192 | return nil, nil, errs.Errorf( |
| 193 | "invalid access token, audience mismatch, got %q, expected %q or %q", |
| 194 | claims.Audience, |
| 195 | AccessTokenAudience, |
| 196 | OAuth2AccessTokenAudience, |
| 197 | ) |
| 198 | } |
| 199 | |
| 200 | account, err := in.store.GetAccountByEmail(ctx, claims.Subject) |
| 201 | if err != nil { |
| 202 | return nil, nil, errs.Errorf("failed to find principal %q in the access token", claims.Subject) |
| 203 | } |
| 204 | if account == nil { |
| 205 | return nil, nil, errs.Errorf("principal %q not exists in the access token", claims.Subject) |
| 206 | } |
| 207 | if account.MemberDeleted { |
| 208 | return nil, nil, errs.Errorf("principal %q has been deactivated by administrators", account.Email) |
| 209 | } |
| 210 | |
| 211 | // Verify workspace membership. |
| 212 | // We always require workspace_id in the claims even for non-SaaS (single workspace) mode |
| 213 | if claims.WorkspaceID == "" { |
| 214 | return nil, nil, errs.New("empty workspace in the token") |
| 215 | } |
| 216 | if err := in.verifyWorkspaceMembership(ctx, claims.WorkspaceID, account); err != nil { |
| 217 | return nil, nil, err |
| 218 | } |
| 219 | |
| 220 | // Convert to UserMessage for context storage. |
| 221 | user, err := in.store.ResolvePrincipalAsUser(ctx, account) |
| 222 | if err != nil { |
| 223 | return nil, nil, err |
| 224 | } |
no test coverage detected