package oidc import ( "context" "encoding/json" "errors" "net/url" "time" "github.com/ory/fosite" fositeoauth2 "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/pkce" "github.com/ory/fosite/handler/rfc8628" fositestorage "github.com/ory/fosite/storage" "github.com/pocket-id/pocket-id/backend/internal/model" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" "gorm.io/gorm" "gorm.io/gorm/clause" ) const ( sessionKindAuthorizeCode = "authorize_code" sessionKindAccessToken = "access_token" sessionKindRefreshToken = "refresh_token" sessionKindPKCE = "pkce" sessionKindOpenID = "openid" sessionKindPAR = "par" sessionKindDeviceCode = "device_code" sessionKindUserCode = "user_code" ) var ( _ fosite.Storage = (*Store)(nil) _ fosite.PARStorage = (*Store)(nil) _ fositeoauth2.CoreStorage = (*Store)(nil) _ fositeoauth2.TokenRevocationStorage = (*Store)(nil) _ rfc8628.RFC8628CoreStorage = (*Store)(nil) _ openid.OpenIDConnectRequestStorage = (*Store)(nil) _ pkce.PKCERequestStorage = (*Store)(nil) _ fositestorage.Transactional = (*Store)(nil) ) // NewStore creates the fosite storage. Exported for packages that need to seed or // revoke sessions (e.g. the e2e test service). func NewStore(db *gorm.DB) *Store { return &Store{db: db} } type Store struct { db *gorm.DB } type storedRequester struct { Authorize bool `json:"authorize,omitempty"` ID string `json:"id"` RequestedAt time.Time `json:"requested_at"` ClientID string `json:"client_id"` RequestedScope fosite.Arguments `json:"requested_scope,omitempty"` GrantedScope fosite.Arguments `json:"granted_scope,omitempty"` Form url.Values `json:"form,omitempty"` Session *Session `json:"session,omitempty"` RequestedAudience fosite.Arguments `json:"requested_audience,omitempty"` GrantedAudience fosite.Arguments `json:"granted_audience,omitempty"` Device bool `json:"device,omitempty"` UserCodeState fosite.UserCodeState `json:"user_code_state,omitempty"` ResponseTypes fosite.Arguments `json:"response_types,omitempty"` RedirectURI string `json:"redirect_uri,omitempty"` State string `json:"state,omitempty"` HandledResponseTypes fosite.Arguments `json:"handled_response_types,omitempty"` ResponseMode fosite.ResponseModeType `json:"response_mode,omitempty"` DefaultResponseMode fosite.ResponseModeType `json:"default_response_mode,omitempty"` } // Satisfies fosite.Storage func (s *Store) GetClient(ctx context.Context, id string) (fosite.Client, error) { var client model.OidcClient err := s.dbFor(ctx). Preload("AllowedUserGroups"). First(&client, "id = ?", id). Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fosite.ErrNotFound } if err != nil { return nil, err } return Client{OidcClient: client}, nil } func (s *Store) ClientAssertionJWTValid(ctx context.Context, jti string) error { var count int64 err := s.dbFor(ctx). Model(&clientAssertionJTI{}). Where("jti = ? AND expires_at > ?", jti, datatype.DateTime(time.Now())). Count(&count). Error if err != nil { return err } if count > 0 { return fosite.ErrJTIKnown } return nil } func (s *Store) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error { err := s.dbFor(ctx).Create(&clientAssertionJTI{ JTI: jti, ExpiresAt: datatype.DateTime(exp), }).Error if errors.Is(err, gorm.ErrDuplicatedKey) { return fosite.ErrJTIKnown } return err } // Satisfies fositeoauth2.CoreStorage func (s *Store) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) error { return s.upsertSession(ctx, sessionKindAuthorizeCode, code, request, "", true, fosite.AuthorizeCode) } func (s *Store) GetAuthorizeCodeSession(ctx context.Context, code string, _ fosite.Session) (fosite.Requester, error) { request, active, err := s.getRequesterSession(ctx, sessionKindAuthorizeCode, code) if err != nil { return nil, err } if !active { return request, fosite.ErrInvalidatedAuthorizeCode } return request, nil } func (s *Store) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error { return s.deactivateSession(ctx, sessionKindAuthorizeCode, code) } func (s *Store) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error { return s.upsertSession(ctx, sessionKindAccessToken, signature, request, "", true, fosite.AccessToken) } func (s *Store) GetAccessTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { request, _, err := s.getRequesterSession(ctx, sessionKindAccessToken, signature) return request, err } func (s *Store) DeleteAccessTokenSession(ctx context.Context, signature string) error { return s.deleteSession(ctx, sessionKindAccessToken, signature) } func (s *Store) CreateRefreshTokenSession(ctx context.Context, signature string, accessSignature string, request fosite.Requester) error { return s.upsertSession(ctx, sessionKindRefreshToken, signature, request, accessSignature, true, fosite.RefreshToken) } func (s *Store) GetRefreshTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { request, active, err := s.getRequesterSession(ctx, sessionKindRefreshToken, signature) if err != nil { return nil, err } if !active { return request, fosite.ErrInactiveToken } return request, nil } func (s *Store) DeleteRefreshTokenSession(ctx context.Context, signature string) error { return s.deleteSession(ctx, sessionKindRefreshToken, signature) } func (s *Store) RotateRefreshToken(ctx context.Context, requestID string, refreshTokenSignature string) error { if err := s.deactivateSession(ctx, sessionKindRefreshToken, refreshTokenSignature); err != nil { return err } return s.RevokeAccessToken(ctx, requestID) } // Satisfies fositeoauth2.TokenRevocationStorage func (s *Store) RevokeRefreshToken(ctx context.Context, requestID string) error { return s.dbFor(ctx). Model(&OAuth2Session{}). Where("kind = ? AND request_id = ?", sessionKindRefreshToken, requestID). Update("active", false). Error } func (s *Store) RevokeAccessToken(ctx context.Context, requestID string) error { return s.dbFor(ctx). Where("kind = ? AND request_id = ?", sessionKindAccessToken, requestID). Delete(&OAuth2Session{}). Error } func (s *Store) RevokeSessionsByIDTokenHint(ctx context.Context, userID, clientID, idTokenJTI string) error { _, jtiMatches, err := s.findUserClientRequestIDs(ctx, userID, clientID, idTokenJTI) if err != nil { return err } return s.revokeRequestIDs(ctx, jtiMatches) } func RevokeUserClientSessions(ctx context.Context, db *gorm.DB, userID, clientID string) error { s := NewStore(db) requestIDs, _, err := s.findUserClientRequestIDs(ctx, userID, clientID, "") if err != nil { return err } return s.revokeRequestIDs(ctx, requestIDs) } func (s *Store) findUserClientRequestIDs(ctx context.Context, userID, clientID, idTokenJTI string) (candidates []string, jtiMatches []string, err error) { var sessions []OAuth2Session err = s.dbFor(ctx). Where("kind = ? AND active = ?", sessionKindRefreshToken, true). Find(&sessions). Error if err != nil { return nil, nil, err } candidateRequestIDs := map[string]struct{}{} matchingRequestIDs := map[string]struct{}{} for _, session := range sessions { requester, err := s.decodeRequester(ctx, session.RequestData) if err != nil { return nil, nil, err } requestSession := requester.GetSession() if requestSession == nil || requester.GetClient().GetID() != clientID || requestSession.GetSubject() != userID { continue } // Add all sessions that match the user and client candidateRequestIDs[session.RequestID] = struct{}{} // Only add sessions that also match the ID token hint JTI if storedSession, ok := requestSession.(*Session); ok && idTokenJTI != "" && storedSession.IDTokenClaims().JTI == idTokenJTI { matchingRequestIDs[session.RequestID] = struct{}{} } } return mapKeys(candidateRequestIDs), mapKeys(matchingRequestIDs), nil } func (s *Store) revokeRequestIDs(ctx context.Context, requestIDs []string) error { if len(requestIDs) == 0 { return nil } if err := s.dbFor(ctx). Model(&OAuth2Session{}). Where("kind = ? AND request_id IN ?", sessionKindRefreshToken, requestIDs). Update("active", false). Error; err != nil { return err } return s.dbFor(ctx). Where("kind = ? AND request_id IN ?", sessionKindAccessToken, requestIDs). Delete(&OAuth2Session{}). Error } func mapKeys(m map[string]struct{}) []string { keys := make([]string, 0, len(m)) for key := range m { keys = append(keys, key) } return keys } // Satisfies pkce.PKCERequestStorage func (s *Store) CreatePKCERequestSession(ctx context.Context, signature string, requester fosite.Requester) error { // PKCE sessions share the authorize code lifespan so abandoned ones expire and get cleaned up return s.upsertSession(ctx, sessionKindPKCE, signature, requester, "", true, fosite.AuthorizeCode) } func (s *Store) GetPKCERequestSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { request, _, err := s.getRequesterSession(ctx, sessionKindPKCE, signature) return request, err } func (s *Store) DeletePKCERequestSession(ctx context.Context, signature string) error { return s.deleteSession(ctx, sessionKindPKCE, signature) } // Satisfies openid.OpenIDConnectRequestStorage func (s *Store) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) error { return s.upsertSession(ctx, sessionKindOpenID, authorizeCode, requester, "", true, fosite.AuthorizeCode) } func (s *Store) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, _ fosite.Requester) (fosite.Requester, error) { request, _, err := s.getRequesterSession(ctx, sessionKindOpenID, authorizeCode) if errors.Is(err, fosite.ErrNotFound) { return nil, openid.ErrNoSessionFound } return request, err } func (s *Store) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error { return s.deleteSession(ctx, sessionKindOpenID, authorizeCode) } // Satisfies fosite.PARStorage func (s *Store) CreatePARSession(ctx context.Context, requestURI string, request fosite.AuthorizeRequester) error { return s.upsertAuthorizeSession(ctx, sessionKindPAR, requestURI, request, true, fosite.PushedAuthorizeRequestContext) } func (s *Store) GetPARSession(ctx context.Context, requestURI string) (fosite.AuthorizeRequester, error) { session, err := s.getSession(ctx, sessionKindPAR, requestURI) if err != nil { return nil, err } if !session.Active || session.ExpiresAt == nil || session.ExpiresAt.ToTime().Before(time.Now()) { return nil, fosite.ErrNotFound } result := s.dbFor(ctx). Model(&OAuth2Session{}). Where("kind = ? AND key = ? AND active = ?", sessionKindPAR, requestURI, true). Update("active", false) if result.Error != nil { return nil, result.Error } if result.RowsAffected == 0 { return nil, fosite.ErrNotFound } return s.decodeAuthorizeRequester(ctx, session.RequestData) } func (s *Store) DeletePARSession(ctx context.Context, requestURI string) error { return s.deleteSession(ctx, sessionKindPAR, requestURI) } // Satisfies rfc8628.RFC8628CoreStorage func (s *Store) CreateDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, request fosite.DeviceRequester) error { requestData, err := s.encodeDeviceRequester(request) if err != nil { return err } if _, err := s.getSession(ctx, sessionKindUserCode, userCodeSignature); err == nil { return fosite.ErrExistingUserCodeSignature } else if !errors.Is(err, fosite.ErrNotFound) { return err } expDeviceCode := expiresAt(request.GetSession(), fosite.DeviceCode) expUserCode := expiresAt(request.GetSession(), fosite.UserCode) if err := s.storeSession(ctx, sessionKindDeviceCode, deviceCodeSignature, request.GetID(), "", true, requestData, expDeviceCode); err != nil { return err } return s.storeSession(ctx, sessionKindUserCode, userCodeSignature, request.GetID(), "", true, requestData, expUserCode) } func (s *Store) GetDeviceCodeSession(ctx context.Context, signature string, _ fosite.Session) (fosite.DeviceRequester, error) { request, active, err := s.getDeviceRequesterSession(ctx, sessionKindDeviceCode, signature) if err != nil { return nil, err } if !active { return request, fosite.ErrInvalidatedDeviceCode } return request, nil } func (s *Store) InvalidateDeviceCodeSession(ctx context.Context, signature string) error { session, err := s.getSession(ctx, sessionKindDeviceCode, signature) if err != nil { return err } // Only flip rows that are still active so two concurrent token requests for the same // device code cannot both pass the single-use check and each mint a token set. result := s.dbFor(ctx). Model(&OAuth2Session{}). Where("kind IN ? AND request_id = ? AND active = ?", []string{sessionKindDeviceCode, sessionKindUserCode}, session.RequestID, true). Update("active", false) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fosite.ErrNotFound } return nil } func (s *Store) GetDeviceCodeSessionByUserCodeSignature(ctx context.Context, signature string) (fosite.DeviceRequester, error) { request, active, err := s.getDeviceRequesterSession(ctx, sessionKindUserCode, signature) if err != nil { return nil, err } if !active { return nil, fosite.ErrNotFound } return request, nil } func (s *Store) AcceptDeviceCodeSessionByUserCodeSignature(ctx context.Context, signature string, request fosite.DeviceRequester) (string, error) { userCodeSession, err := s.getSession(ctx, sessionKindUserCode, signature) if err != nil { return "", err } request.SetUserCodeState(fosite.UserCodeAccepted) requestData, err := s.encodeDeviceRequester(request) if err != nil { return "", err } deviceCodeSession, err := s.getSessionByRequestID(ctx, sessionKindDeviceCode, userCodeSession.RequestID) if err != nil { return "", err } err = s.dbFor(ctx). Model(&OAuth2Session{}). Where("kind IN ? AND request_id = ?", []string{sessionKindDeviceCode, sessionKindUserCode}, userCodeSession.RequestID). Updates(map[string]any{ "request_data": requestData, }). Error if err != nil { return "", err } return deviceCodeSession.Key, nil } // Satisfies fositestorage.Transactional func (s *Store) BeginTX(ctx context.Context) (context.Context, error) { tx := s.db.WithContext(ctx).Begin() if tx.Error != nil { return ctx, tx.Error } return contextWithTx(ctx, tx), nil } func (s *Store) Commit(ctx context.Context) error { tx, ok := ctx.Value(txContextKey{}).(*gorm.DB) if !ok { return nil } return tx.Commit().Error } func (s *Store) Rollback(ctx context.Context) error { tx, ok := ctx.Value(txContextKey{}).(*gorm.DB) if !ok { return nil } return tx.Rollback().Error } func (s *Store) upsertSession(ctx context.Context, kind string, key string, requester fosite.Requester, accessTokenSignature string, active bool, expiresAtKey fosite.TokenType) error { requestData, err := s.encodeRequester(requester) if err != nil { return err } return s.storeSession(ctx, kind, key, requester.GetID(), accessTokenSignature, active, requestData, expiresAt(requester.GetSession(), expiresAtKey)) } func (s *Store) upsertAuthorizeSession(ctx context.Context, kind string, key string, requester fosite.AuthorizeRequester, active bool, expiresAtKey fosite.TokenType) error { requestData, err := s.encodeAuthorizeRequester(requester) if err != nil { return err } return s.storeSession(ctx, kind, key, requester.GetID(), "", active, requestData, expiresAt(requester.GetSession(), expiresAtKey)) } func (s *Store) storeSession(ctx context.Context, kind string, key string, requestID string, accessTokenSignature string, active bool, requestData string, exp *datatype.DateTime) error { session := OAuth2Session{ Kind: kind, Key: key, RequestID: requestID, AccessTokenSignature: accessTokenSignature, Active: active, RequestData: requestData, ExpiresAt: exp, } return s.dbFor(ctx). Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "kind"}, {Name: "key"}}, DoUpdates: clause.AssignmentColumns([]string{ "request_id", "access_token_signature", "active", "request_data", "expires_at", }), }). Create(&session). Error } func (s *Store) getRequesterSession(ctx context.Context, kind string, key string) (fosite.Requester, bool, error) { session, err := s.getSession(ctx, kind, key) if err != nil { return nil, false, err } requester, err := s.decodeRequester(ctx, session.RequestData) if err != nil { return nil, false, err } return requester, session.Active, nil } func (s *Store) getDeviceRequesterSession(ctx context.Context, kind string, key string) (fosite.DeviceRequester, bool, error) { session, err := s.getSession(ctx, kind, key) if err != nil { return nil, false, err } requester, err := s.decodeDeviceRequester(ctx, session.RequestData) if err != nil { return nil, false, err } return requester, session.Active, nil } func (s *Store) getSession(ctx context.Context, kind string, key string) (session OAuth2Session, err error) { err = s.dbFor(ctx). Where("kind = ? AND key = ?", kind, key). First(&session). Error if errors.Is(err, gorm.ErrRecordNotFound) { return session, fosite.ErrNotFound } if err != nil { return session, err } return session, nil } func (s *Store) getSessionByRequestID(ctx context.Context, kind string, requestID string) (session OAuth2Session, err error) { err = s.dbFor(ctx). Where("kind = ? AND request_id = ?", kind, requestID). First(&session). Error if errors.Is(err, gorm.ErrRecordNotFound) { return session, fosite.ErrNotFound } if err != nil { return session, err } return session, nil } func (s *Store) deleteSession(ctx context.Context, kind string, key string) error { return s.dbFor(ctx). Where("kind = ? AND key = ?", kind, key). Delete(&OAuth2Session{}). Error } func (s *Store) deactivateSession(ctx context.Context, kind string, key string) error { result := s.dbFor(ctx). Model(&OAuth2Session{}). Where("kind = ? AND key = ? AND active = ?", kind, key, true). Update("active", false) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fosite.ErrNotFound } return nil } func (s *Store) encodeRequester(requester fosite.Requester) (string, error) { stored, err := s.storedRequesterFromRequester(requester) if err != nil { return "", err } data, err := json.Marshal(stored) if err != nil { return "", err } return string(data), nil } func (s *Store) encodeAuthorizeRequester(requester fosite.AuthorizeRequester) (string, error) { stored, err := s.storedRequesterFromRequester(requester) if err != nil { return "", err } stored.Authorize = true stored.ResponseTypes = cloneArguments(requester.GetResponseTypes()) if redirectURI := requester.GetRedirectURI(); redirectURI != nil { stored.RedirectURI = redirectURI.String() } stored.State = requester.GetState() stored.ResponseMode = requester.GetResponseMode() stored.DefaultResponseMode = requester.GetDefaultResponseMode() if ar, ok := requester.(*fosite.AuthorizeRequest); ok { stored.HandledResponseTypes = cloneArguments(ar.HandledResponseTypes) } data, err := json.Marshal(stored) if err != nil { return "", err } return string(data), nil } func (s *Store) encodeDeviceRequester(requester fosite.DeviceRequester) (string, error) { stored, err := s.storedRequesterFromRequester(requester) if err != nil { return "", err } stored.Device = true stored.UserCodeState = requester.GetUserCodeState() data, err := json.Marshal(stored) if err != nil { return "", err } return string(data), nil } func (s *Store) storedRequesterFromRequester(requester fosite.Requester) (storedRequester, error) { if requester == nil { return storedRequester{}, fosite.ErrServerError.WithHint("requester must not be nil") } return storedRequester{ ID: requester.GetID(), RequestedAt: requester.GetRequestedAt(), ClientID: requester.GetClient().GetID(), RequestedScope: cloneArguments(requester.GetRequestedScopes()), GrantedScope: cloneArguments(requester.GetGrantedScopes()), Form: sanitizeStoredForm(requester.GetRequestForm()), Session: cloneSession(requester.GetSession()), RequestedAudience: cloneArguments(requester.GetRequestedAudience()), GrantedAudience: cloneArguments(requester.GetGrantedAudience()), }, nil } func (s *Store) decodeRequester(ctx context.Context, data string) (fosite.Requester, error) { var stored storedRequester if err := json.Unmarshal([]byte(data), &stored); err != nil { return nil, err } if stored.Authorize { return s.requesterFromStoredAuthorize(ctx, stored) } return s.requesterFromStored(ctx, stored) } func (s *Store) decodeAuthorizeRequester(ctx context.Context, data string) (fosite.AuthorizeRequester, error) { var stored storedRequester if err := json.Unmarshal([]byte(data), &stored); err != nil { return nil, err } stored.Authorize = true return s.requesterFromStoredAuthorize(ctx, stored) } func (s *Store) decodeDeviceRequester(ctx context.Context, data string) (fosite.DeviceRequester, error) { var stored storedRequester if err := json.Unmarshal([]byte(data), &stored); err != nil { return nil, err } stored.Device = true return s.requesterFromStoredDevice(ctx, stored) } func (s *Store) requesterFromStored(ctx context.Context, stored storedRequester) (fosite.Requester, error) { client, err := s.GetClient(ctx, stored.ClientID) if err != nil { return nil, err } request := fosite.NewRequest() request.ID = stored.ID request.RequestedAt = stored.RequestedAt request.Client = client request.RequestedScope = cloneArguments(stored.RequestedScope) request.GrantedScope = cloneArguments(stored.GrantedScope) request.Form = cloneValues(stored.Form) request.Session = stored.Session request.RequestedAudience = cloneArguments(stored.RequestedAudience) request.GrantedAudience = cloneArguments(stored.GrantedAudience) return request, nil } func (s *Store) requesterFromStoredDevice(ctx context.Context, stored storedRequester) (fosite.DeviceRequester, error) { requester, err := s.requesterFromStored(ctx, stored) if err != nil { return nil, err } base := requester.(*fosite.Request) request := fosite.NewDeviceRequest() request.Request = *base request.UserCodeState = stored.UserCodeState return request, nil } func (s *Store) requesterFromStoredAuthorize(ctx context.Context, stored storedRequester) (fosite.AuthorizeRequester, error) { requester, err := s.requesterFromStored(ctx, stored) if err != nil { return nil, err } base := requester.(*fosite.Request) request := fosite.NewAuthorizeRequest() request.Request = *base request.ResponseTypes = cloneArguments(stored.ResponseTypes) request.State = stored.State request.HandledResponseTypes = cloneArguments(stored.HandledResponseTypes) request.ResponseMode = stored.ResponseMode request.DefaultResponseMode = stored.DefaultResponseMode if stored.RedirectURI != "" { redirectURI, err := url.Parse(stored.RedirectURI) if err != nil { return nil, err } request.RedirectURI = redirectURI } return request, nil } func cloneSession(session fosite.Session) *Session { if session == nil { return nil } if s, ok := session.(*Session); ok { cloned := s.Clone() if typed, ok := cloned.(*Session); ok { return typed } } cloned := NewEmptySession() cloned.Subject = session.GetSubject() for _, tokenType := range []fosite.TokenType{ fosite.AccessToken, fosite.RefreshToken, fosite.AuthorizeCode, fosite.IDToken, fosite.PushedAuthorizeRequestContext, fosite.DeviceCode, fosite.UserCode, } { if exp := session.GetExpiresAt(tokenType); !exp.IsZero() { cloned.SetExpiresAt(tokenType, exp) } } return cloned } func cloneArguments(arguments fosite.Arguments) fosite.Arguments { if len(arguments) == 0 { return fosite.Arguments{} } cloned := make(fosite.Arguments, len(arguments)) copy(cloned, arguments) return cloned } func sanitizeStoredForm(values url.Values) url.Values { cloned := cloneValues(values) cloned.Del("client_secret") cloned.Del("client_assertion") return cloned } func cloneValues(values url.Values) url.Values { if len(values) == 0 { return url.Values{} } cloned := make(url.Values, len(values)) for key, value := range values { cloned[key] = append([]string(nil), value...) } return cloned } func expiresAt(session fosite.Session, tokenType fosite.TokenType) *datatype.DateTime { if session == nil || tokenType == "" { return nil } exp := session.GetExpiresAt(tokenType) if exp.IsZero() { return nil } return new(datatype.DateTime(exp)) } func (s *Store) dbFor(ctx context.Context) *gorm.DB { return dbFromContext(ctx, s.db) }