(c *echo.Context, client *store.OAuth2ClientMessage, req *tokenRequest)
| 93 | } |
| 94 | |
| 95 | func (s *Service) handleAuthorizationCodeGrant(c *echo.Context, client *store.OAuth2ClientMessage, req *tokenRequest) error { |
| 96 | ctx := c.Request().Context() |
| 97 | |
| 98 | // Validate grant type is allowed |
| 99 | if !slices.Contains(client.Config.GrantTypes, "authorization_code") { |
| 100 | return oauth2Error(c, http.StatusBadRequest, "unauthorized_client", "client not authorized for authorization_code grant") |
| 101 | } |
| 102 | |
| 103 | // Validate code |
| 104 | if req.Code == "" { |
| 105 | return oauth2Error(c, http.StatusBadRequest, "invalid_request", "code is required") |
| 106 | } |
| 107 | |
| 108 | authCode, err := s.store.GetOAuth2AuthorizationCode(ctx, client.ClientID, req.Code) |
| 109 | if err != nil { |
| 110 | return oauth2Error(c, http.StatusInternalServerError, "server_error", "failed to lookup code") |
| 111 | } |
| 112 | if authCode == nil { |
| 113 | return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "invalid or expired code") |
| 114 | } |
| 115 | |
| 116 | // Validate code belongs to this client BEFORE deleting |
| 117 | // This prevents DoS where attacker with stolen code invalidates it for legitimate client |
| 118 | if authCode.ClientID != client.ClientID { |
| 119 | return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "code was not issued to this client") |
| 120 | } |
| 121 | |
| 122 | // Validate code not expired |
| 123 | if time.Now().After(authCode.ExpiresAt) { |
| 124 | // Delete expired code |
| 125 | if err := s.store.DeleteOAuth2AuthorizationCode(ctx, client.ClientID, req.Code); err != nil { |
| 126 | slog.Warn("failed to delete expired OAuth2 authorization code", slog.String("code", req.Code), log.BBError(err)) |
| 127 | } |
| 128 | return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "code has expired") |
| 129 | } |
| 130 | |
| 131 | // Validate redirect_uri matches |
| 132 | if req.RedirectURI != authCode.Config.RedirectUri { |
| 133 | return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "redirect_uri mismatch") |
| 134 | } |
| 135 | |
| 136 | // Validate PKCE |
| 137 | if req.CodeVerifier == "" { |
| 138 | return oauth2Error(c, http.StatusBadRequest, "invalid_request", "code_verifier is required") |
| 139 | } |
| 140 | if !verifyPKCE(req.CodeVerifier, authCode.Config.CodeChallenge, authCode.Config.CodeChallengeMethod) { |
| 141 | return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "invalid code_verifier") |
| 142 | } |
| 143 | |
| 144 | // Delete code after all validations pass (single use) |
| 145 | if err := s.store.DeleteOAuth2AuthorizationCode(ctx, client.ClientID, req.Code); err != nil { |
| 146 | slog.Warn("failed to delete OAuth2 authorization code after use", slog.String("code", req.Code), log.BBError(err)) |
| 147 | } |
| 148 | |
| 149 | // Get user |
| 150 | user, err := s.store.GetUserByEmail(ctx, authCode.UserEmail) |
| 151 | if err != nil || user == nil { |
| 152 | return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "user not found") |
no test coverage detected