mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-13 05:09:05 +02:00
chore(deps): update Fosite
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/pocket-id/fosite"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/pocket-id/fosite"
|
||||
)
|
||||
|
||||
type parHandler struct {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/pocket-id/fosite"
|
||||
)
|
||||
|
||||
type tokenHandler struct {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user