diff --git a/internal/token/usecase.go b/internal/token/usecase.go index 126438b..2224eb3 100644 --- a/internal/token/usecase.go +++ b/internal/token/usecase.go @@ -7,9 +7,24 @@ import ( "source.toby3d.me/website/oauth/internal/domain" ) -type UseCase interface { - Verify(ctx context.Context, accessToken string) (*domain.Token, error) - Revoke(ctx context.Context, accessToken string) error -} +type ( + GenerateOptions struct { + ClientID string + Me string + Scopes []string + NonceLength int + } + + UseCase interface { + // Generate generates a new Token based on the session data. + Generate(ctx context.Context, opts GenerateOptions) (*domain.Token, error) + + // Verify checks the AccessToken and returns the associated information. + Verify(ctx context.Context, accessToken string) (*domain.Token, error) + + // Revoke revokes the AccessToken and blocks its further use. + Revoke(ctx context.Context, accessToken string) error + } +) var ErrRevoke = errors.New("this token has been revoked") diff --git a/internal/token/usecase/token_ucase.go b/internal/token/usecase/token_ucase.go index befcdd8..69061c9 100644 --- a/internal/token/usecase/token_ucase.go +++ b/internal/token/usecase/token_ucase.go @@ -3,6 +3,7 @@ package usecase import ( "context" "strings" + "time" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwt" @@ -11,19 +12,60 @@ import ( "source.toby3d.me/website/oauth/internal/config" "source.toby3d.me/website/oauth/internal/domain" + "source.toby3d.me/website/oauth/internal/random" "source.toby3d.me/website/oauth/internal/token" ) -type tokenUseCase struct { - tokens token.Repository - configer config.UseCase +type ( + Config struct { + Configer config.UseCase + Tokens token.Repository + } + + tokenUseCase struct { + configer config.UseCase + tokens token.Repository + } +) + +func NewTokenUseCase(config Config) token.UseCase { + return &tokenUseCase{ + configer: config.Configer, + tokens: config.Tokens, + } } -func NewTokenUseCase(tokens token.Repository, configer config.UseCase) token.UseCase { - return &tokenUseCase{ - tokens: tokens, - configer: configer, +// Generate generates a new Token based on the session data. +func (useCase *tokenUseCase) Generate(ctx context.Context, opts token.GenerateOptions) (*domain.Token, error) { + nonce, err := random.String(opts.NonceLength) + if err != nil { + return nil, errors.Wrap(err, "cannot generate code") } + + t := jwt.New() + now := time.Now().UTC().Round(time.Second) + + t.Set(jwt.IssuerKey, opts.ClientID) + t.Set(jwt.SubjectKey, opts.Me) + t.Set(jwt.ExpirationKey, now.Add(useCase.configer.GetIndieAuthAccessTokenExpirationTime())) + t.Set(jwt.NotBeforeKey, now) + t.Set(jwt.IssuedAtKey, now) + t.Set("scope", strings.Join(opts.Scopes, " ")) + t.Set("nonce", nonce) + + token, err := jwt.Sign(t, + jwa.SignatureAlgorithm(useCase.configer.GetIndieAuthJWTSigningAlgorithm()), + []byte(useCase.configer.GetIndieAuthJWTSecret())) + if err != nil { + return nil, errors.Wrap(err, "cannot sign a new access token") + } + + return &domain.Token{ + Scopes: opts.Scopes, + AccessToken: string(token), + ClientID: opts.ClientID, + Me: opts.Me, + }, nil } func (useCase *tokenUseCase) Verify(ctx context.Context, accessToken string) (*domain.Token, error) { diff --git a/internal/token/usecase/token_ucase_test.go b/internal/token/usecase/token_ucase_test.go index 3adb48f..f8ffdca 100644 --- a/internal/token/usecase/token_ucase_test.go +++ b/internal/token/usecase/token_ucase_test.go @@ -2,10 +2,11 @@ package usecase_test import ( "context" + "strings" "sync" "testing" - "github.com/spf13/viper" + "github.com/lestrrat-go/jwx/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,12 +18,38 @@ import ( ucase "source.toby3d.me/website/oauth/internal/token/usecase" ) -func TestVerify(t *testing.T) { +func TestGenerate(t *testing.T) { t.Parallel() - v := viper.New() - v.Set("indieauth.jwtSigningAlgorithm", "HS256") - v.Set("indieauth.jwtSecret", "hackme") + configer := configucase.NewConfigUseCase(configrepo.NewViperConfigRepository(domain.TestConfig(t))) + options := token.GenerateOptions{ + ClientID: "https://app.example.com/", + Me: "https://user.example.net/", + Scopes: []string{"create", "update", "delete"}, + NonceLength: 42, + } + + result, err := ucase.NewTokenUseCase(ucase.Config{ + Configer: configer, + Tokens: nil, + }).Generate(context.TODO(), options) + require.NoError(t, err) + assert.Equal(t, options.ClientID, result.ClientID) + assert.Equal(t, options.Me, result.Me) + assert.Equal(t, options.Scopes, result.Scopes) + + token, err := jwt.ParseString(result.AccessToken) + require.NoError(t, err) + assert.Equal(t, options.Me, token.Subject()) + assert.Equal(t, options.ClientID, token.Issuer()) + + scope, ok := token.Get("scope") + require.True(t, ok) + assert.Equal(t, strings.Join(options.Scopes, " "), scope) +} + +func TestVerify(t *testing.T) { + t.Parallel() repo := repository.NewMemoryTokenRepository(new(sync.Map)) useCase := ucase.NewTokenUseCase(repo, configucase.NewConfigUseCase(configrepo.NewViperConfigRepository(v)))