ContainerImage.Pinniped/internal/federationdomain/strategy/dynamic_oauth2_hmac_strategy.go
Ryan Richard 86c791b8a6 reorganize federation domain packages to be more intuitive
Co-authored-by: Benjamin A. Petersen <ben@benjaminapetersen.me>
2023-09-11 11:11:52 -07:00

162 lines
5.9 KiB
Go

// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package strategy
import (
"context"
"strings"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
"github.com/ory/fosite/handler/oauth2"
errorsx "github.com/pkg/errors"
"go.pinniped.dev/internal/federationdomain/storage"
)
const (
pinAccessTokenPrefix = "pin_at_" // "Pinniped access token" abbreviated.
oryAccessTokenPrefix = "ory_at_"
pinRefreshTokenPrefix = "pin_rt_" // "Pinniped refresh token" abbreviated.
oryRefreshTokenPrefix = "ory_rt_"
pinAuthcodePrefix = "pin_ac_" // "Pinniped authorization code" abbreviated.
oryAuthcodePrefix = "ory_ac_"
)
// DynamicOauth2HMACStrategy is an oauth2.CoreStrategy that can dynamically load an HMAC key to sign
// stuff (access tokens, refresh tokens, and auth codes). We want this dynamic capability since our
// controllers for loading FederationDomain's and signing keys run in parallel, and thus the signing key
// might not be ready when an FederationDomain is otherwise ready.
//
// If we ever update FederationDomain's to hold their signing key, we might not need this type, since we
// could have an invariant that routes to an FederationDomain's endpoints are only wired up if an
// FederationDomain has a valid signing key.
//
// Tokens start with a custom prefix to make them identifiable as tokens when seen by a user
// out of context, such as when accidentally committed to a GitHub repo. After we implemented the
// custom prefix feature, fosite later added the same feature, but did not make the prefix customizable.
// Therefore, this code has been updated to replace the fosite prefix with our custom prefix.
type DynamicOauth2HMACStrategy struct {
fositeConfig *fosite.Config
keyFunc func() []byte
}
var _ oauth2.CoreStrategy = &DynamicOauth2HMACStrategy{}
func NewDynamicOauth2HMACStrategy(
fositeConfig *fosite.Config,
keyFunc func() []byte,
) *DynamicOauth2HMACStrategy {
return &DynamicOauth2HMACStrategy{
fositeConfig: fositeConfig,
keyFunc: keyFunc,
}
}
func replacePrefix(s, prefixToReplace, newPrefix string) string {
return newPrefix + strings.TrimPrefix(s, prefixToReplace)
}
func (s *DynamicOauth2HMACStrategy) AccessTokenSignature(ctx context.Context, token string) string {
return s.delegate().AccessTokenSignature(ctx, token)
}
func (s *DynamicOauth2HMACStrategy) GenerateAccessToken(
ctx context.Context,
requester fosite.Requester,
) (string, string, error) {
token, sig, err := s.delegate().GenerateAccessToken(ctx, requester)
if err == nil {
if !strings.HasPrefix(token, oryAccessTokenPrefix) {
// This would only happen if fosite changed how it generates tokens. Defensive programming here.
return "", "", errorsx.WithStack(fosite.ErrInvalidTokenFormat.
WithDebugf("Generated token does not have expected prefix"))
}
token = replacePrefix(token, oryAccessTokenPrefix, pinAccessTokenPrefix)
}
return token, sig, err
}
func (s *DynamicOauth2HMACStrategy) ValidateAccessToken(
ctx context.Context,
requester fosite.Requester,
token string,
) error {
if !strings.HasPrefix(token, pinAccessTokenPrefix) {
return errorsx.WithStack(fosite.ErrInvalidTokenFormat.
WithDebugf("Access token did not have prefix %q", pinAccessTokenPrefix))
}
return s.delegate().ValidateAccessToken(ctx, requester, replacePrefix(token, pinAccessTokenPrefix, oryAccessTokenPrefix))
}
func (s *DynamicOauth2HMACStrategy) RefreshTokenSignature(ctx context.Context, token string) string {
return s.delegate().RefreshTokenSignature(ctx, token)
}
func (s *DynamicOauth2HMACStrategy) GenerateRefreshToken(
ctx context.Context,
requester fosite.Requester,
) (string, string, error) {
token, sig, err := s.delegate().GenerateRefreshToken(ctx, requester)
if err == nil {
if !strings.HasPrefix(token, oryRefreshTokenPrefix) {
// This would only happen if fosite changed how it generates tokens. Defensive programming here.
return "", "", errorsx.WithStack(fosite.ErrInvalidTokenFormat.
WithDebugf("Generated token does not have expected prefix"))
}
token = replacePrefix(token, oryRefreshTokenPrefix, pinRefreshTokenPrefix)
}
return token, sig, err
}
func (s *DynamicOauth2HMACStrategy) ValidateRefreshToken(
ctx context.Context,
requester fosite.Requester,
token string,
) error {
if !strings.HasPrefix(token, pinRefreshTokenPrefix) {
return errorsx.WithStack(fosite.ErrInvalidTokenFormat.
WithDebugf("Refresh token did not have prefix %q", pinRefreshTokenPrefix))
}
return s.delegate().ValidateRefreshToken(ctx, requester, replacePrefix(token, pinRefreshTokenPrefix, oryRefreshTokenPrefix))
}
func (s *DynamicOauth2HMACStrategy) AuthorizeCodeSignature(ctx context.Context, token string) string {
return s.delegate().AuthorizeCodeSignature(ctx, token)
}
func (s *DynamicOauth2HMACStrategy) GenerateAuthorizeCode(
ctx context.Context,
requester fosite.Requester,
) (string, string, error) {
authcode, sig, err := s.delegate().GenerateAuthorizeCode(ctx, requester)
if err == nil {
if !strings.HasPrefix(authcode, oryAuthcodePrefix) {
// This would only happen if fosite changed how it generates tokens. Defensive programming here.
return "", "", errorsx.WithStack(fosite.ErrInvalidTokenFormat.
WithDebugf("Generated token does not have expected prefix"))
}
authcode = replacePrefix(authcode, oryAuthcodePrefix, pinAuthcodePrefix)
}
return authcode, sig, err
}
func (s *DynamicOauth2HMACStrategy) ValidateAuthorizeCode(
ctx context.Context,
requester fosite.Requester,
token string,
) error {
if !strings.HasPrefix(token, pinAuthcodePrefix) {
return errorsx.WithStack(fosite.ErrInvalidTokenFormat.
WithDebugf("Authorization code did not have prefix %q", pinAuthcodePrefix))
}
return s.delegate().ValidateAuthorizeCode(ctx, requester, replacePrefix(token, pinAuthcodePrefix, oryAuthcodePrefix))
}
func (s *DynamicOauth2HMACStrategy) delegate() *oauth2.HMACSHAStrategy {
return compose.NewOAuth2HMACStrategy(storage.NewDynamicGlobalSecretConfig(s.fositeConfig, s.keyFunc))
}