MCPcopy Create free account
hub / github.com/Mnexa-AI/e2a / principalFromOAuthToken

Method principalFromOAuthToken

internal/agent/api.go:745–809  ·  view source on GitHub ↗

principalFromOAuthToken validates an ate2a_-prefixed bearer via fosite's IntrospectToken (which derives the signature using the same strategy that issued the token, looks up the row via our Storage, and checks revoked/expired) and resolves it to a SCOPED principal. The granted OAuth scope is author

(r *http.Request, bearer string)

Source from the content-addressed store, hash-verified

743// Every failure wraps errOAuthBearerInvalid so the response layer reliably
744// classifies these as OAuth-bearer rejections and emits the OAuth challenge.
745func (a *API) principalFromOAuthToken(r *http.Request, bearer string) (*identity.Principal, error) {
746 if a.oauthProvider == nil {
747 // OAuth not enabled on this deployment. Fail closed rather
748 // than fall through to the API-key path (which would compare
749 // the ate2a_ token against the api_keys hash and miss — a
750 // slower path to the same 401, with a less actionable log).
751 return nil, fmt.Errorf("%w: provider not configured", errOAuthBearerInvalid)
752 }
753 session := &oauth.Session{}
754 tu, ar, err := a.oauthProvider.IntrospectToken(r.Context(), bearer, fosite.AccessToken, session)
755 if err != nil {
756 // Preserve fosite's typed error via %w so writeAuthError can
757 // errors.Is(...) against fosite.ErrTokenExpired below.
758 return nil, fmt.Errorf("%w: %w", errOAuthBearerInvalid, err)
759 }
760 // Defense in depth: fosite v0.49's IntrospectToken with
761 // tokenUse=AccessToken doesn't HARD-reject a refresh-token row —
762 // it falls back to refresh-token validation if access fails. We
763 // rely on table separation in storage to keep them disjoint, but
764 // an explicit check at the seam means a future fosite/storage
765 // refactor can't silently break the type guard.
766 if tu != fosite.AccessToken {
767 return nil, fmt.Errorf("%w: token is not an access token (got %q)", errOAuthBearerInvalid, tu)
768 }
769 // Trust the session, not the request — the session was loaded
770 // from the DB row fosite hydrated.
771 sess, ok := ar.GetSession().(*oauth.Session)
772 if !ok || sess.UserID == "" {
773 return nil, fmt.Errorf("%w: session missing user_id", errOAuthBearerInvalid)
774 }
775 u, err := a.store.GetUserByID(r.Context(), sess.UserID)
776 if err != nil {
777 // Wrap so writeAuthError emits the OAuth challenge instead of
778 // a bare 401 (the bearer that got us here was valid; the user
779 // row vanished out from under it).
780 return nil, fmt.Errorf("%w: user lookup: %v", errOAuthBearerInvalid, err)
781 }
782
783 granted := ar.GetGrantedScopes()
784 switch {
785 case granted.Has(identity.ScopeAccount):
786 return &identity.Principal{User: u, Scope: identity.ScopeAccount}, nil
787 case granted.Has(identity.ScopeAgent):
788 // Confine to the consent-bound agent. An agent-scoped token with no
789 // bound agent is malformed — reject rather than fall back to anything
790 // broader.
791 if sess.AgentEmail == "" {
792 return nil, fmt.Errorf("%w: agent-scoped token has no bound agent", errOAuthBearerInvalid)
793 }
794 ag, err := a.store.GetAgentByEmail(r.Context(), identity.NormalizeEmail(sess.AgentEmail))
795 if err != nil || ag == nil {
796 return nil, fmt.Errorf("%w: bound agent not found", errOAuthBearerInvalid)
797 }
798 // Ownership re-check: the bound agent must still belong to the token's
799 // user (defends against an agent reassigned/deleted-and-recreated under
800 // a different owner after consent).
801 if ag.UserID != u.ID {
802 return nil, fmt.Errorf("%w: bound agent not owned by token user", errOAuthBearerInvalid)

Callers 1

authenticatePrincipalMethod · 0.95

Calls 3

NormalizeEmailFunction · 0.92
GetUserByIDMethod · 0.80
GetAgentByEmailMethod · 0.65

Tested by

no test coverage detected