chore(deps): update Fosite

This commit is contained in:
Elias Schneider
2026-10-11 00:16:20 +02:00
parent 54f28f86d0
commit fd7951f70e
44 changed files with 387 additions and 212 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"strings"
"testing"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/require"
)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/utils"
+7 -7
View File
@@ -4,15 +4,15 @@ import (
"context"
"slices"
"github.com/ory/fosite"
fositeoauth2 "github.com/ory/fosite/handler/oauth2"
"github.com/pocket-id/fosite"
fositeoauth2 "github.com/pocket-id/fosite/handler/oauth2"
)
// NewAccessTokenStrategy applies Pocket ID's identity-audience policy to the provided access token strategy
func NewAccessTokenStrategy(coreStrategy fositeoauth2.CoreStrategy, issuer string) fositeoauth2.CoreStrategy {
func NewAccessTokenStrategy(accessTokenStrategy fositeoauth2.AccessTokenStrategy, issuer string) fositeoauth2.AccessTokenStrategy {
return identityAudienceAccessTokenStrategy{
CoreStrategy: coreStrategy,
issuer: issuer,
AccessTokenStrategy: accessTokenStrategy,
issuer: issuer,
}
}
@@ -58,10 +58,10 @@ func (r identityAudienceRequester) GetGrantedAudience() fosite.Arguments {
// identityAudienceAccessTokenStrategy wraps the access token strategy so an access token granted an identity scope also lists the issuer in its audience, keeping the self-contained JWT consistent with what is persisted for introspection and userinfo
type identityAudienceAccessTokenStrategy struct {
fositeoauth2.CoreStrategy
fositeoauth2.AccessTokenStrategy
issuer string
}
func (s identityAudienceAccessTokenStrategy) GenerateAccessToken(ctx context.Context, requester fosite.Requester) (string, string, error) {
return s.CoreStrategy.GenerateAccessToken(ctx, withIdentityAudience(requester, s.issuer))
return s.AccessTokenStrategy.GenerateAccessToken(ctx, withIdentityAudience(requester, s.issuer))
}
@@ -3,7 +3,7 @@ package oidc
import (
"testing"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/assert"
"github.com/pocket-id/pocket-id/backend/internal/model"
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"slices"
"strings"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"gorm.io/gorm"
)
@@ -9,7 +9,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/authz"
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/utils"
@@ -11,7 +11,7 @@ import (
"strings"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
@@ -6,7 +6,7 @@ import (
"testing"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"slices"
"strings"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"errors"
"slices"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model"
"gorm.io/gorm"
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"slices"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"github.com/pocket-id/pocket-id/backend/internal/authz"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
@@ -0,0 +1,82 @@
package oidc
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/require"
)
func TestDeviceCodeExchangeConsumesOpenIDConnectSession(t *testing.T) {
const (
userID = "test-user"
clientID = "test-client"
)
service, store, provider, userCode, deviceCode := newTestDeviceServiceWithCode(t, clientID, userID, false, nil)
require.NoError(t, service.acceptDeviceCode(t.Context(), userCode, userID, "phr", time.Now().UTC(), "", requestMeta{}))
signature, err := provider.deviceStrategy.DeviceCodeSignature(t.Context(), deviceCode)
require.NoError(t, err)
// The approved authorization keeps an OpenID Connect session that expires together with the device code
var deviceSession, openIDSession OAuth2Session
require.NoError(t, store.db.First(&deviceSession, "kind = ? AND key = ?", sessionKindDeviceCode, signature).Error)
require.NoError(t, store.db.First(&openIDSession, "kind = ? AND key = ?", sessionKindOpenID, signature).Error)
require.NotNil(t, deviceSession.ExpiresAt)
require.NotNil(t, openIDSession.ExpiresAt)
require.True(t, deviceSession.ExpiresAt.ToTime().Equal(openIDSession.ExpiresAt.ToTime()))
// The token exchange issues an ID token and consumes the OpenID Connect session
response, err := exchangeDeviceCode(t, provider, clientID, deviceCode)
require.NoError(t, err)
require.NotNil(t, response.GetExtra("id_token"))
var count int64
require.NoError(t, store.db.Model(&OAuth2Session{}).Where("kind = ?", sessionKindOpenID).Count(&count).Error)
require.Zero(t, count)
require.NoError(t, store.db.Model(&OAuth2Session{}).Where("kind = ? AND request_id = ?", sessionKindAccessToken, deviceSession.RequestID).Count(&count).Error)
require.EqualValues(t, 1, count)
// A replayed device code is rejected before an OpenID Connect session could be reused
_, err = exchangeDeviceCode(t, provider, clientID, deviceCode)
require.ErrorIs(t, err, fosite.ErrInvalidGrant)
// The replay revokes the access token that the first exchange issued
require.NoError(t, store.db.Model(&OAuth2Session{}).Where("kind = ? AND request_id = ?", sessionKindAccessToken, deviceSession.RequestID).Count(&count).Error)
require.Zero(t, count)
}
func TestStoreInvalidateDeviceCodeSessionReportsLostRaceAsInvalidGrant(t *testing.T) {
_, store, provider, _, deviceCode := newTestDeviceServiceWithCode(t, "test-client", "test-user", false, nil)
signature, err := provider.deviceStrategy.DeviceCodeSignature(t.Context(), deviceCode)
require.NoError(t, err)
// The first redemption wins and a concurrent second one must not mint another token set
require.NoError(t, store.InvalidateDeviceCodeSession(t.Context(), signature))
require.ErrorIs(t, store.InvalidateDeviceCodeSession(t.Context(), signature), fosite.ErrInvalidGrant)
}
// exchangeDeviceCode redeems the device code at the token endpoint like a polling device would
func exchangeDeviceCode(t *testing.T, provider *oidcProvider, clientID, deviceCode string) (fosite.AccessResponder, error) {
t.Helper()
form := url.Values{
"grant_type": {string(fosite.GrantTypeDeviceCode)},
"device_code": {deviceCode},
"client_id": {clientID},
}
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/api/oidc/token", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
accessRequest, err := provider.NewAccessRequest(t.Context(), req, NewEmptySession())
if err != nil {
return nil, err
}
return provider.NewAccessResponse(t.Context(), accessRequest)
}
+17 -2
View File
@@ -7,8 +7,8 @@ import (
"strings"
"time"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/rfc8628"
"github.com/pocket-id/fosite"
"github.com/pocket-id/fosite/handler/rfc8628"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
@@ -135,6 +135,9 @@ func (s *deviceService) acceptDeviceCode(ctx context.Context, userCode, userID,
session := NewAuthenticatedSession(userID, authenticationMethod, authenticationTime, request.GetRequestedAt())
// Keep the lifetimes of the pending device authorization, so its codes and the OpenID Connect session created below still expire
copyDeviceCodeExpiry(request.GetSession(), session)
if err = s.claimsService.applyIDTokenClaims(ctx, session, request.GetGrantedScopes()); err != nil {
return err
}
@@ -166,6 +169,18 @@ func (s *deviceService) acceptDeviceCode(ctx context.Context, userCode, userID,
})
}
// copyDeviceCodeExpiry copies the device and user code expiry from the pending device authorization's session to the session that replaces it
func copyDeviceCodeExpiry(from fosite.Session, to *Session) {
if from == nil {
return
}
for _, tokenType := range []fosite.TokenType{fosite.DeviceCode, fosite.UserCode} {
if expiresAt := from.GetExpiresAt(tokenType); !expiresAt.IsZero() {
to.SetExpiresAt(tokenType, expiresAt)
}
}
}
func (s *deviceService) loadDeviceAuthorizationUser(ctx context.Context, userID string) (model.User, error) {
tx := s.db.Begin()
defer func() {
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"testing"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"github.com/pocket-id/pocket-id/backend/internal/model"
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
+1 -1
View File
@@ -3,7 +3,7 @@ package oidc
import (
"context"
"github.com/ory/fosite/handler/rfc8628"
"github.com/pocket-id/fosite/handler/rfc8628"
"github.com/pocket-id/pocket-id/backend/internal/utils"
)
@@ -17,7 +17,7 @@ import (
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
@@ -14,7 +14,7 @@ import (
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/require"
"github.com/pocket-id/pocket-id/backend/internal/model"
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
)
type introspectionHandler struct {
@@ -16,7 +16,7 @@ import (
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/require"
"github.com/pocket-id/pocket-id/backend/internal/model"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"log/slog"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
)
type parHandler struct {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"time"
jwxjwt "github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/utils"
+15 -17
View File
@@ -8,11 +8,11 @@ import (
"net/url"
"time"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
fositeoauth2 "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
"github.com/pocket-id/fosite"
"github.com/pocket-id/fosite/compose"
fositeoauth2 "github.com/pocket-id/fosite/handler/oauth2"
"github.com/pocket-id/fosite/handler/openid"
"github.com/pocket-id/fosite/token/jwt"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"golang.org/x/crypto/hkdf"
)
@@ -65,15 +65,12 @@ func newProvider(store *Store, authenticator *federatedClientAuthenticator, sign
return SigningKeyFromSigner(signer)
}
sig := newJWTSigner(keyGetter)
coreStrategy := compose.NewOAuth2HMACStrategy(fositeConfig)
deviceStrategy := &deviceStrategy{DefaultDeviceStrategy: compose.NewDeviceStrategy(fositeConfig)}
defaultAccessTokenStrategy := &fositeoauth2.DefaultJWTStrategy{
Signer: sig,
HMACSHAStrategy: coreStrategy,
Config: fositeConfig,
}
rfc9068AccessTokenStrategy := &fositeoauth2.RFC9068JWTStrategy{
DefaultJWTStrategy: defaultAccessTokenStrategy,
DefaultJWTStrategy: &fositeoauth2.DefaultJWTStrategy{
Signer: sig,
Config: fositeConfig,
},
}
// Apply Pocket ID's identity-audience policy outside Fosite's reusable RFC 9068 token profile
@@ -85,11 +82,12 @@ func newProvider(store *Store, authenticator *federatedClientAuthenticator, sign
provider := compose.Compose(
fositeConfig,
store,
&compose.CommonStrategy{
CoreStrategy: accessTokenStrategy,
RFC8628CodeStrategy: deviceStrategy,
OpenIDConnectTokenStrategy: idTokenStrategy,
Signer: sig,
&strategyProvider{
opaque: compose.NewOAuth2HMACStrategy(fositeConfig),
accessToken: accessTokenStrategy,
device: deviceStrategy,
idToken: idTokenStrategy,
Signer: sig,
},
compose.OAuth2AuthorizeExplicitFactory,
compose.OAuth2ClientCredentialsGrantFactory,
+2 -2
View File
@@ -17,8 +17,8 @@ import (
"time"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/ory/fosite"
fositeoauth2 "github.com/ory/fosite/handler/oauth2"
"github.com/pocket-id/fosite"
fositeoauth2 "github.com/pocket-id/fosite/handler/oauth2"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
"github.com/pocket-id/fosite"
"github.com/pocket-id/fosite/compose"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
+4 -4
View File
@@ -5,10 +5,10 @@ import (
"time"
"uuid"
"github.com/ory/fosite"
fositeoauth2 "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
fositejwt "github.com/ory/fosite/token/jwt"
"github.com/pocket-id/fosite"
fositeoauth2 "github.com/pocket-id/fosite/handler/oauth2"
"github.com/pocket-id/fosite/handler/openid"
fositejwt "github.com/pocket-id/fosite/token/jwt"
)
var _ openid.Session = (*Session)(nil)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
jose "github.com/go-jose/go-jose/v4"
fositejwt "github.com/ory/fosite/token/jwt"
fositejwt "github.com/pocket-id/fosite/token/jwt"
)
// SigningKeyFromSigner wraps the raw signing key in a *jose.JSONWebKey that carries the
+83 -51
View File
@@ -10,12 +10,11 @@ import (
"slices"
"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/fosite"
fositeoauth2 "github.com/pocket-id/fosite/handler/oauth2"
"github.com/pocket-id/fosite/handler/openid"
"github.com/pocket-id/fosite/handler/pkce"
"github.com/pocket-id/fosite/handler/rfc8628"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"gorm.io/gorm"
@@ -35,14 +34,17 @@ const (
)
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)
_ fosite.Storage = (*Store)(nil)
_ fosite.ClientManager = (*Store)(nil)
_ fosite.PARStorageProvider = (*Store)(nil)
_ fositeoauth2.AuthorizeCodeStorageProvider = (*Store)(nil)
_ fositeoauth2.AccessTokenStorageProvider = (*Store)(nil)
_ fositeoauth2.RefreshTokenStorageProvider = (*Store)(nil)
_ fositeoauth2.TokenRevocationStorageProvider = (*Store)(nil)
_ rfc8628.DeviceAuthStorageProvider = (*Store)(nil)
_ openid.OpenIDConnectRequestStorageProvider = (*Store)(nil)
_ pkce.PKCERequestStorageProvider = (*Store)(nil)
_ fosite.Transactional = (*Store)(nil)
)
// NewStore creates the fosite storage. Exported for packages that need to seed or
@@ -88,7 +90,7 @@ type storedRequester struct {
DefaultResponseMode fosite.ResponseModeType `json:"default_response_mode,omitempty"`
}
// Satisfies fosite.Storage
// Satisfies fosite.ClientManager
func (s *Store) GetClient(ctx context.Context, id string) (fosite.Client, error) {
tx := s.dbFor(ctx)
@@ -324,14 +326,14 @@ func (s *Store) firstClientByID(ctx context.Context, id string) (model.OidcClien
return client, nil
}
// Satisfies fositeoauth2.CoreStorage
// Satisfies fositeoauth2.AuthorizeCodeStorage, fositeoauth2.AccessTokenStorage and fositeoauth2.RefreshTokenStorage
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) CreateAuthorizeCodeSession(ctx context.Context, signature string, request fosite.Requester) error {
return s.upsertSession(ctx, sessionKindAuthorizeCode, signature, 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)
func (s *Store) GetAuthorizeCodeSession(ctx context.Context, _ string, signature string, _ fosite.Session) (fosite.Requester, error) {
request, active, err := s.getRequesterSession(ctx, sessionKindAuthorizeCode, signature)
if err != nil {
return nil, err
}
@@ -341,8 +343,14 @@ func (s *Store) GetAuthorizeCodeSession(ctx context.Context, code string, _ fosi
return request, nil
}
func (s *Store) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error {
return s.deactivateSession(ctx, sessionKindAuthorizeCode, code)
func (s *Store) InvalidateAuthorizeCodeSession(ctx context.Context, _ string, signature string) error {
err := s.deactivateSession(ctx, sessionKindAuthorizeCode, signature)
// The code was active when the token request read it, so a missing active row means a concurrent request redeemed it first
if errors.Is(err, fosite.ErrNotFound) {
return fosite.ErrInvalidGrant.WithHint("The authorization code has already been used.")
}
return err
}
func (s *Store) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error {
@@ -524,19 +532,25 @@ func (s *Store) CreatePKCERequestSession(ctx context.Context, signature string,
return s.upsertSession(ctx, sessionKindPKCE, signature, requester, "", true, fosite.AuthorizeCode)
}
func (s *Store) GetPKCERequestSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error) {
func (s *Store) GetPKCERequestSession(ctx context.Context, _ string, 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 {
func (s *Store) DeletePKCERequestSession(ctx context.Context, _ string, 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)
// Sessions of the device authorization grant are keyed by the device code signature and expire together with the device code
// An expiry ensures the cleanup job removes sessions whose code is never redeemed
expiresAtKey := fosite.AuthorizeCode
if session := requester.GetSession(); session != nil && session.GetExpiresAt(fosite.AuthorizeCode).IsZero() {
expiresAtKey = fosite.DeviceCode
}
return s.upsertSession(ctx, sessionKindOpenID, authorizeCode, requester, "", true, expiresAtKey)
}
func (s *Store) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, _ fosite.Requester) (fosite.Requester, error) {
@@ -584,7 +598,7 @@ func (s *Store) DeletePARSession(ctx context.Context, requestURI string) error {
return s.deleteSession(ctx, sessionKindPAR, requestURI)
}
// Satisfies rfc8628.RFC8628CoreStorage
// Satisfies rfc8628.DeviceAuthStorage
func (s *Store) CreateDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, request fosite.DeviceRequester) error {
requestData, err := s.encodeDeviceRequester(request)
@@ -633,7 +647,8 @@ func (s *Store) InvalidateDeviceCodeSession(ctx context.Context, signature strin
return result.Error
}
if result.RowsAffected == 0 {
return fosite.ErrNotFound
// The device code was active when the token request read it, so a concurrent request redeemed it first
return fosite.ErrInvalidGrant.WithHint("The device code has already been used.")
}
return nil
}
@@ -709,30 +724,9 @@ func (s *Store) AcceptDeviceCodeSessionByUserCodeSignature(ctx context.Context,
return deviceCodeSignature, err
}
// 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
// Satisfies fosite.Transactional
func (s *Store) Transaction(ctx context.Context, fn func(ctx context.Context) error) error {
return withTx(ctx, s.db, fn)
}
func (s *Store) upsertSession(ctx context.Context, kind string, key string, requester fosite.Requester, accessTokenSignature string, active bool, expiresAtKey fosite.TokenType) error {
@@ -1104,3 +1098,41 @@ func expiresAt(session fosite.Session, tokenType fosite.TokenType) *datatype.Dat
func (s *Store) dbFor(ctx context.Context) *gorm.DB {
return dbFromContext(ctx, s.db)
}
// Fosite reaches every storage through a provider accessor, and Store implements all of them itself
func (s *Store) FositeClientManager() fosite.ClientManager {
return s
}
func (s *Store) AuthorizeCodeStorage() fositeoauth2.AuthorizeCodeStorage {
return s
}
func (s *Store) AccessTokenStorage() fositeoauth2.AccessTokenStorage {
return s
}
func (s *Store) RefreshTokenStorage() fositeoauth2.RefreshTokenStorage {
return s
}
func (s *Store) TokenRevocationStorage() fositeoauth2.TokenRevocationStorage {
return s
}
func (s *Store) OpenIDConnectRequestStorage() openid.OpenIDConnectRequestStorage {
return s
}
func (s *Store) PKCERequestStorage() pkce.PKCERequestStorage {
return s
}
func (s *Store) DeviceAuthStorage() rfc8628.DeviceAuthStorage {
return s
}
func (s *Store) PARStorage() fosite.PARStorage {
return s
}
+10 -9
View File
@@ -5,8 +5,8 @@ import (
"testing"
"time"
"github.com/ory/fosite"
fositejwt "github.com/ory/fosite/token/jwt"
"github.com/pocket-id/fosite"
fositejwt "github.com/pocket-id/fosite/token/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
@@ -69,8 +69,9 @@ func TestStoreInvalidateAuthorizeCodeSessionIsAtomic(t *testing.T) {
store := NewStore(db, nil)
const (
clientID = "code-client"
code = "auth-code-single-use"
clientID = "code-client"
code = "auth-code-single-use"
signature = "auth-code-single-use-signature"
)
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Code Client"}).Error)
@@ -84,17 +85,17 @@ func TestStoreInvalidateAuthorizeCodeSessionIsAtomic(t *testing.T) {
Form: url.Values{},
Session: session,
}
require.NoError(t, store.CreateAuthorizeCodeSession(t.Context(), code, request))
require.NoError(t, store.CreateAuthorizeCodeSession(t.Context(), signature, request))
// First invalidation wins.
require.NoError(t, store.InvalidateAuthorizeCodeSession(t.Context(), code))
require.NoError(t, store.InvalidateAuthorizeCodeSession(t.Context(), code, signature))
// A second invalidation of the now-inactive code fails closed: a racing token request
// cannot proceed to issue a second set of tokens from the same code.
require.ErrorIs(t, store.InvalidateAuthorizeCodeSession(t.Context(), code), fosite.ErrNotFound)
// cannot proceed to issue a second set of tokens from the same code, and the client gets invalid_grant.
require.ErrorIs(t, store.InvalidateAuthorizeCodeSession(t.Context(), code, signature), fosite.ErrInvalidGrant)
// Reads of the consumed code report it as invalidated so fosite triggers reuse handling.
_, err := store.GetAuthorizeCodeSession(t.Context(), code, nil)
_, err := store.GetAuthorizeCodeSession(t.Context(), code, signature, nil)
require.ErrorIs(t, err, fosite.ErrInvalidatedAuthorizeCode)
}
@@ -0,0 +1,57 @@
package oidc
import (
fositeoauth2 "github.com/pocket-id/fosite/handler/oauth2"
"github.com/pocket-id/fosite/handler/openid"
"github.com/pocket-id/fosite/handler/rfc8628"
"github.com/pocket-id/fosite/token/jwt"
)
// strategyProvider hands every Fosite handler the token strategy it needs
type strategyProvider struct {
opaque fositeoauth2.CoreStrategy
accessToken fositeoauth2.AccessTokenStrategy
device *deviceStrategy
idToken openid.OpenIDConnectTokenStrategy
jwt.Signer
}
var (
_ fositeoauth2.AuthorizeCodeStrategyProvider = (*strategyProvider)(nil)
_ fositeoauth2.AccessTokenStrategyProvider = (*strategyProvider)(nil)
_ fositeoauth2.RefreshTokenStrategyProvider = (*strategyProvider)(nil)
_ openid.OpenIDConnectTokenStrategyProvider = (*strategyProvider)(nil)
_ rfc8628.DeviceRateLimitStrategyProvider = (*strategyProvider)(nil)
_ rfc8628.DeviceCodeStrategyProvider = (*strategyProvider)(nil)
_ rfc8628.UserCodeStrategyProvider = (*strategyProvider)(nil)
_ jwt.Signer = (*strategyProvider)(nil)
)
func (s *strategyProvider) AuthorizeCodeStrategy() fositeoauth2.AuthorizeCodeStrategy {
return s.opaque
}
func (s *strategyProvider) AccessTokenStrategy() fositeoauth2.AccessTokenStrategy {
return s.accessToken
}
func (s *strategyProvider) RefreshTokenStrategy() fositeoauth2.RefreshTokenStrategy {
return s.opaque
}
func (s *strategyProvider) OpenIDConnectTokenStrategy() openid.OpenIDConnectTokenStrategy {
return s.idToken
}
func (s *strategyProvider) DeviceRateLimitStrategy() rfc8628.DeviceRateLimitStrategy {
return s.device
}
func (s *strategyProvider) DeviceCodeStrategy() rfc8628.DeviceCodeStrategy {
return s.device
}
func (s *strategyProvider) UserCodeStrategy() rfc8628.UserCodeStrategy {
return s.device
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"slices"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
)
type tokenHandler struct {
+4 -4
View File
@@ -15,10 +15,10 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
fositeoauth2 "github.com/ory/fosite/handler/oauth2"
fositejwt "github.com/ory/fosite/token/jwt"
"github.com/pocket-id/fosite"
"github.com/pocket-id/fosite/compose"
fositeoauth2 "github.com/pocket-id/fosite/handler/oauth2"
fositejwt "github.com/pocket-id/fosite/token/jwt"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"gorm.io/gorm"
)
@@ -11,7 +11,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"github.com/stretchr/testify/require"
"github.com/pocket-id/pocket-id/backend/internal/model"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/url"
"strings"
"github.com/ory/fosite"
"github.com/pocket-id/fosite"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"github.com/pocket-id/pocket-id/backend/internal/common"
+4 -10
View File
@@ -20,9 +20,9 @@ import (
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
fositejwt "github.com/ory/fosite/token/jwt"
"github.com/pocket-id/fosite"
"github.com/pocket-id/fosite/compose"
fositejwt "github.com/pocket-id/fosite/token/jwt"
"github.com/pocket-id/pocket-id/backend/internal/apikey"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"gorm.io/gorm"
@@ -905,20 +905,14 @@ func seededRefreshTokenFixture(userID string, clientID string, fixtureRefreshTok
}
func (s *TestService) SignAccessToken(ctx context.Context, userID, clientID string, expired bool) (string, error) {
globalSecret, err := oidc.DeriveGlobalSecret(common.EnvConfig.EncryptionKey)
if err != nil {
return "", err
}
fositeConfig := &fosite.Config{
GlobalSecret: globalSecret,
AccessTokenLifespan: AccessTokenDuration,
AccessTokenIssuer: common.EnvConfig.AppURL,
}
coreStrategy := compose.NewOAuth2HMACStrategy(fositeConfig)
keyGetter := func(context.Context) (interface{}, error) {
return oidc.SigningKeyFromSigner(s.jwtService)
}
strategy := oidc.NewAccessTokenStrategy(compose.NewOAuth2RFC9068JWTStrategy(keyGetter, coreStrategy, fositeConfig), common.EnvConfig.AppURL)
strategy := oidc.NewAccessTokenStrategy(compose.NewOAuth2RFC9068JWTStrategy(keyGetter, fositeConfig), common.EnvConfig.AppURL)
expiresAt := time.Now().UTC().Add(AccessTokenDuration)
if expired {