diff --git a/.mockery.yaml b/.mockery.yaml index 20d2471..b2b6a5c 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -15,3 +15,10 @@ packages: pkgname: authkitmocks dir: mocks/authkit filename: principal_action_resolver.go + PrincipalResolver: + config: + template: testify + structname: PrincipalResolver + pkgname: authkitmocks + dir: mocks/authkit + filename: principal_resolver.go diff --git a/exchange/apitoken.go b/exchange/apitoken.go index c7fa4d2..f40b31d 100644 --- a/exchange/apitoken.go +++ b/exchange/apitoken.go @@ -3,12 +3,50 @@ package exchange import ( "context" "errors" - "fmt" "github.com/meigma/authkit" "github.com/meigma/authkit/access/jwt" + "github.com/meigma/authkit/proof/apikey" ) +// APITokenVerifier verifies opaque API tokens and returns their authenticated metadata. +// Implementations must return authkit.ErrUnauthenticated (directly or wrapped) when plaintext is +// not a currently valid token; other errors are treated as internal failures. +type APITokenVerifier interface { + // VerifyAPIToken verifies plaintext and returns its authenticated token metadata. + VerifyAPIToken(ctx context.Context, plaintext string) (apikey.VerifiedToken, error) +} + +// APITokenOptions configures an APITokenExchanger. +type APITokenOptions struct { + // APITokens verifies opaque API tokens. + APITokens APITokenVerifier + + // Principals loads principals authenticated by API tokens. + Principals authkit.PrincipalFinder + + // AccessTokens issues authkit access JWTs. + AccessTokens AccessTokenIssuer +} + +// APITokenRequest describes an API-token exchange request. +type APITokenRequest struct { + // Plaintext is the opaque API token presented for exchange. + Plaintext string +} + +// APITokenResult describes a completed API-token exchange. +type APITokenResult struct { + // APIToken is the verified opaque API token metadata. + APIToken apikey.VerifiedToken + + // Principal is the principal authenticated by APIToken. + Principal authkit.Principal + + // AccessToken is the authkit access JWT issued for Principal. + AccessToken jwt.IssuedToken +} + // APITokenExchanger exchanges opaque API tokens for authkit access JWTs. type APITokenExchanger struct { apiTokens APITokenVerifier @@ -47,6 +85,8 @@ func (e *APITokenExchanger) Exchange(ctx context.Context, req APITokenRequest) ( } principal, err := e.principals.FindPrincipal(ctx, apiToken.PrincipalID) + // Missing principal collapses to the generic unauthenticated response so the API does not + // leak whether a specific PrincipalID exists. if errors.Is(err, authkit.ErrPrincipalNotFound) { return APITokenResult{}, unauthenticated("principal not found") } @@ -67,22 +107,3 @@ func (e *APITokenExchanger) Exchange(ctx context.Context, req APITokenRequest) ( AccessToken: accessToken, }, nil } - -func exchangeError(operation string, err error) error { - if isContextError(err) { - return err - } - if errors.Is(err, authkit.ErrUnauthenticated) || errors.Is(err, authkit.ErrUnresolvedIdentity) { - return err - } - - return fmt.Errorf("%w: %s: %w", authkit.ErrInternal, operation, err) -} - -func unauthenticated(reason string) error { - return fmt.Errorf("%w: %s", authkit.ErrUnauthenticated, reason) -} - -func isContextError(err error) bool { - return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) -} diff --git a/exchange/apitoken_test.go b/exchange/apitoken_test.go index 1991ffb..3b89db5 100644 --- a/exchange/apitoken_test.go +++ b/exchange/apitoken_test.go @@ -7,24 +7,19 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/meigma/authkit" - "github.com/meigma/authkit/access/jwt" "github.com/meigma/authkit/exchange" - "github.com/meigma/authkit/internal/authtest" + authkitmocks "github.com/meigma/authkit/mocks/authkit" "github.com/meigma/authkit/proof/apikey" "github.com/meigma/authkit/store/memory" ) -const ( - testPrincipalID = "principal_1" - testTokenID = "access-token-123" -) - func TestNewAPITokenExchangerValidatesDependencies(t *testing.T) { apiTokens := fakeAPITokenVerifier{} - principals := fakePrincipalFinder{} + principals := authkitmocks.NewPrincipalFinder(t) accessTokens := fakeAccessTokenIssuer{} tests := []struct { @@ -104,40 +99,11 @@ func TestAPITokenExchangerExchangesTokenForAccessJWT(t *testing.T) { assert.Equal(t, principal.ID, verified.PrincipalID) } -func TestAPITokenExchangerDoesNotRequireIdentityLink(t *testing.T) { - ctx := context.Background() - store := memory.NewStore() - principal, err := store.CreatePrincipal(ctx, authkit.CreatePrincipalRequest{ - Kind: authkit.PrincipalKindService, - DisplayName: "notes service", - }) - require.NoError(t, err) - apiTokens, err := apikey.NewService(store, apikey.WithClock(fixedTime)) - require.NoError(t, err) - apiToken, err := apiTokens.IssueToken(ctx, apikey.IssueRequest{ - PrincipalID: principal.ID, - Name: "bootstrap token", - ExpiresAt: fixedTime().Add(time.Hour), - }) - require.NoError(t, err) - accessTokens, _ := newAccessJWTIssuerAndVerifier(t) - exchanger := newAPITokenExchanger(t, exchange.APITokenOptions{ - APITokens: apiTokens, - Principals: store, - AccessTokens: accessTokens, - }) - - result, err := exchanger.Exchange(ctx, exchange.APITokenRequest{ - Plaintext: apiToken.Plaintext, - }) - - require.NoError(t, err) - assert.Equal(t, principal.ID, result.Principal.ID) -} - func TestAPITokenExchangerRejectsMissingPrincipal(t *testing.T) { - ctx := context.Background() - store := memory.NewStore() + principals := authkitmocks.NewPrincipalFinder(t) + principals.EXPECT(). + FindPrincipal(mock.Anything, "missing"). + Return(authkit.Principal{}, authkit.ErrPrincipalNotFound) accessTokens, _ := newAccessJWTIssuerAndVerifier(t) exchanger := newAPITokenExchanger(t, exchange.APITokenOptions{ APITokens: fakeAPITokenVerifier{ @@ -147,11 +113,11 @@ func TestAPITokenExchangerRejectsMissingPrincipal(t *testing.T) { ExpiresAt: fixedTime().Add(time.Hour), }, }, - Principals: store, + Principals: principals, AccessTokens: accessTokens, }) - result, err := exchanger.Exchange(ctx, exchange.APITokenRequest{ + result, err := exchanger.Exchange(context.Background(), exchange.APITokenRequest{ Plaintext: "ak_token_secret", }) @@ -193,35 +159,39 @@ func TestAPITokenExchangerWrapsInternalFailures(t *testing.T) { } tests := []struct { - name string - opts exchange.APITokenOptions - want error + name string + setupOpts func(t *testing.T) exchange.APITokenOptions + want error }{ { name: "principal finder failure", - opts: exchange.APITokenOptions{ - APITokens: fakeAPITokenVerifier{ - token: apiToken, - }, - Principals: fakePrincipalFinder{ - err: storeErr, - }, - AccessTokens: fakeAccessTokenIssuer{}, + setupOpts: func(t *testing.T) exchange.APITokenOptions { + t.Helper() + principals := authkitmocks.NewPrincipalFinder(t) + principals.EXPECT(). + FindPrincipal(mock.Anything, testPrincipalID). + Return(authkit.Principal{}, storeErr) + return exchange.APITokenOptions{ + APITokens: fakeAPITokenVerifier{token: apiToken}, + Principals: principals, + AccessTokens: fakeAccessTokenIssuer{}, + } }, want: storeErr, }, { name: "access token issuer failure", - opts: exchange.APITokenOptions{ - APITokens: fakeAPITokenVerifier{ - token: apiToken, - }, - Principals: fakePrincipalFinder{ - principal: principal, - }, - AccessTokens: fakeAccessTokenIssuer{ - err: issuerErr, - }, + setupOpts: func(t *testing.T) exchange.APITokenOptions { + t.Helper() + principals := authkitmocks.NewPrincipalFinder(t) + principals.EXPECT(). + FindPrincipal(mock.Anything, testPrincipalID). + Return(principal, nil) + return exchange.APITokenOptions{ + APITokens: fakeAPITokenVerifier{token: apiToken}, + Principals: principals, + AccessTokens: fakeAccessTokenIssuer{err: issuerErr}, + } }, want: issuerErr, }, @@ -229,7 +199,7 @@ func TestAPITokenExchangerWrapsInternalFailures(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - exchanger := newAPITokenExchanger(t, tt.opts) + exchanger := newAPITokenExchanger(t, tt.setupOpts(t)) result, err := exchanger.Exchange(context.Background(), exchange.APITokenRequest{ Plaintext: "ak_token_secret", @@ -245,7 +215,7 @@ func TestAPITokenExchangerWrapsInternalFailures(t *testing.T) { func TestAPITokenExchangerPassesThroughContextErrors(t *testing.T) { exchanger := newAPITokenExchanger(t, exchange.APITokenOptions{ APITokens: fakeAPITokenVerifier{}, - Principals: fakePrincipalFinder{}, + Principals: authkitmocks.NewPrincipalFinder(t), AccessTokens: fakeAccessTokenIssuer{}, }) ctx, cancel := context.WithCancel(context.Background()) @@ -259,63 +229,3 @@ func TestAPITokenExchangerPassesThroughContextErrors(t *testing.T) { require.NotErrorIs(t, err, authkit.ErrInternal) assert.Empty(t, result) } - -func newAPITokenExchanger(t *testing.T, opts exchange.APITokenOptions) *exchange.APITokenExchanger { - t.Helper() - - exchanger, err := exchange.NewAPITokenExchanger(opts) - require.NoError(t, err) - - return exchanger -} - -func newAccessJWTIssuerAndVerifier(t *testing.T) (*jwt.Issuer, *jwt.Verifier) { - t.Helper() - - return authtest.NewAccessJWTIssuerAndVerifier( - t, - authtest.WithAccessJWTTokenID(func() (string, error) { - return testTokenID, nil - }), - ) -} - -type fakeAPITokenVerifier struct { - token apikey.VerifiedToken - err error -} - -func (f fakeAPITokenVerifier) VerifyAPIToken( - context.Context, - string, -) (apikey.VerifiedToken, error) { - return f.token, f.err -} - -type fakePrincipalFinder struct { - principal authkit.Principal - err error -} - -func (f fakePrincipalFinder) FindPrincipal( - context.Context, - string, -) (authkit.Principal, error) { - return f.principal, f.err -} - -type fakeAccessTokenIssuer struct { - token jwt.IssuedToken - err error -} - -func (f fakeAccessTokenIssuer) IssueToken( - context.Context, - jwt.IssueRequest, -) (jwt.IssuedToken, error) { - return f.token, f.err -} - -func fixedTime() time.Time { - return authtest.FixedTime() -} diff --git a/exchange/errors.go b/exchange/errors.go new file mode 100644 index 0000000..8d1d660 --- /dev/null +++ b/exchange/errors.go @@ -0,0 +1,39 @@ +package exchange + +import ( + "context" + "errors" + "fmt" + + "github.com/meigma/authkit" +) + +// exchangeError classifies err raised during operation. Context-cancellation errors and authkit +// sentinel errors (ErrUnauthenticated, ErrUnresolvedIdentity) are returned unchanged so callers +// can detect them with [errors.Is]; anything else is wrapped as ErrInternal with operation as +// breadcrumb. +func exchangeError(operation string, err error) error { + // Context errors are returned unchanged so callers can distinguish cancellation/deadline from + // a legitimate collaborator failure. + if isContextError(err) { + return err + } + // Authentication and identity-resolution sentinels are returned unchanged so callers can + // branch on them via [errors.Is]; wrapping would hide the sentinel behind ErrInternal. + if errors.Is(err, authkit.ErrUnauthenticated) || errors.Is(err, authkit.ErrUnresolvedIdentity) { + return err + } + + return fmt.Errorf("%w: %s: %w", authkit.ErrInternal, operation, err) +} + +// unauthenticated wraps reason with authkit.ErrUnauthenticated so callers can recognise the +// failure via [errors.Is]. +func unauthenticated(reason string) error { + return fmt.Errorf("%w: %s", authkit.ErrUnauthenticated, reason) +} + +// isContextError reports whether err is a context cancellation or deadline error. +func isContextError(err error) bool { + return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) +} diff --git a/exchange/helpers_test.go b/exchange/helpers_test.go new file mode 100644 index 0000000..3dd8d47 --- /dev/null +++ b/exchange/helpers_test.go @@ -0,0 +1,95 @@ +package exchange_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/meigma/authkit" + "github.com/meigma/authkit/access/jwt" + "github.com/meigma/authkit/exchange" + "github.com/meigma/authkit/internal/authtest" + "github.com/meigma/authkit/proof/apikey" +) + +const ( + testPrincipalID = "principal_1" + testTokenID = "access-token-123" +) + +func newAPITokenExchanger(t *testing.T, opts exchange.APITokenOptions) *exchange.APITokenExchanger { + t.Helper() + + exchanger, err := exchange.NewAPITokenExchanger(opts) + require.NoError(t, err) + + return exchanger +} + +func newIdentityExchanger(t *testing.T, opts exchange.IdentityOptions) *exchange.IdentityExchanger { + t.Helper() + + exchanger, err := exchange.NewIdentityExchanger(opts) + require.NoError(t, err) + + return exchanger +} + +func newAccessJWTIssuerAndVerifier(t *testing.T) (*jwt.Issuer, *jwt.Verifier) { + t.Helper() + + return authtest.NewAccessJWTIssuerAndVerifier( + t, + authtest.WithAccessJWTTokenID(func() (string, error) { + return testTokenID, nil + }), + ) +} + +func testExchangeIdentity() authkit.Identity { + return authkit.Identity{ + Provider: "https://issuer.example", + Subject: "user-123", + Claims: map[string]any{ + "email": "ada@example.test", + }, + } +} + +func testExchangePrincipal() authkit.Principal { + return authkit.Principal{ + ID: testPrincipalID, + Kind: authkit.PrincipalKindUser, + DisplayName: "Ada Lovelace", + } +} + +func fixedTime() time.Time { + return authtest.FixedTime() +} + +type fakeAPITokenVerifier struct { + token apikey.VerifiedToken + err error +} + +func (f fakeAPITokenVerifier) VerifyAPIToken( + context.Context, + string, +) (apikey.VerifiedToken, error) { + return f.token, f.err +} + +type fakeAccessTokenIssuer struct { + token jwt.IssuedToken + err error +} + +func (f fakeAccessTokenIssuer) IssueToken( + context.Context, + jwt.IssueRequest, +) (jwt.IssuedToken, error) { + return f.token, f.err +} diff --git a/exchange/identity.go b/exchange/identity.go index 3da41bd..14d797b 100644 --- a/exchange/identity.go +++ b/exchange/identity.go @@ -9,6 +9,33 @@ import ( "github.com/meigma/authkit/access/jwt" ) +// IdentityOptions configures an IdentityExchanger. +type IdentityOptions struct { + // Resolver resolves or provisions verified external identities. + Resolver authkit.PrincipalResolver + + // AccessTokens issues authkit access JWTs. + AccessTokens AccessTokenIssuer +} + +// IdentityRequest describes an identity exchange request. +type IdentityRequest struct { + // Identity is the verified external identity presented for exchange. + Identity authkit.Identity +} + +// IdentityResult describes a completed identity exchange. +type IdentityResult struct { + // Identity is the verified external identity exchanged for an access JWT. + Identity authkit.Identity + + // Principal is the principal resolved or provisioned for Identity. + Principal authkit.Principal + + // AccessToken is the authkit access JWT issued for Principal. + AccessToken jwt.IssuedToken +} + // IdentityExchanger exchanges verified external identities for authkit access JWTs. type IdentityExchanger struct { resolver authkit.PrincipalResolver @@ -40,9 +67,14 @@ func (e *IdentityExchanger) Exchange(ctx context.Context, req IdentityRequest) ( if err != nil { return IdentityResult{}, exchangeError("resolve identity", err) } + // Defend against a misbehaving PrincipalResolver that returns (nil, nil): without a principal + // there is nothing to issue a JWT for, and continuing would dereference nil below. if principal == nil { return IdentityResult{}, fmt.Errorf("%w: resolve identity returned nil principal", authkit.ErrInternal) } + // Defend against a resolver returning a principal with no ID; a principal with no ID would + // produce a JWT with an empty subject and bypass downstream authorization that keys off + // Principal.ID. if principal.ID == "" { return IdentityResult{}, fmt.Errorf("%w: resolved principal ID is required", authkit.ErrInternal) } diff --git a/exchange/identity_test.go b/exchange/identity_test.go index a01a58d..ed25b76 100644 --- a/exchange/identity_test.go +++ b/exchange/identity_test.go @@ -7,15 +7,17 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/meigma/authkit" "github.com/meigma/authkit/access/jwt" "github.com/meigma/authkit/exchange" + authkitmocks "github.com/meigma/authkit/mocks/authkit" ) func TestNewIdentityExchangerValidatesDependencies(t *testing.T) { - resolver := fakeIdentityResolver{} + resolver := authkitmocks.NewPrincipalResolver(t) accessTokens := fakeAccessTokenIssuer{} tests := []struct { @@ -55,10 +57,10 @@ func TestIdentityExchangerExchangesResolvedIdentityForAccessJWT(t *testing.T) { JWT: "access.jwt", ExpiresAt: fixedTime().Add(time.Hour), } + resolver := authkitmocks.NewPrincipalResolver(t) + resolver.EXPECT().ResolveIdentity(mock.Anything, identity).Return(&principal, nil) exchanger := newIdentityExchanger(t, exchange.IdentityOptions{ - Resolver: fakeIdentityResolver{ - principal: principal, - }, + Resolver: resolver, AccessTokens: fakeAccessTokenIssuer{ token: accessToken, }, @@ -75,10 +77,12 @@ func TestIdentityExchangerExchangesResolvedIdentityForAccessJWT(t *testing.T) { } func TestIdentityExchangerPassesThroughUnresolvedIdentity(t *testing.T) { + resolver := authkitmocks.NewPrincipalResolver(t) + resolver.EXPECT(). + ResolveIdentity(mock.Anything, mock.Anything). + Return(nil, authkit.ErrUnresolvedIdentity) exchanger := newIdentityExchanger(t, exchange.IdentityOptions{ - Resolver: fakeIdentityResolver{ - err: authkit.ErrUnresolvedIdentity, - }, + Resolver: resolver, AccessTokens: fakeAccessTokenIssuer{}, }) @@ -94,31 +98,40 @@ func TestIdentityExchangerPassesThroughUnresolvedIdentity(t *testing.T) { func TestIdentityExchangerWrapsInternalFailures(t *testing.T) { resolverErr := errors.New("resolver failed") issuerErr := errors.New("issuer failed") + principal := testExchangePrincipal() tests := []struct { - name string - opts exchange.IdentityOptions - want error + name string + setupOpts func(t *testing.T) exchange.IdentityOptions + want error }{ { name: "resolver failure", - opts: exchange.IdentityOptions{ - Resolver: fakeIdentityResolver{ - err: resolverErr, - }, - AccessTokens: fakeAccessTokenIssuer{}, + setupOpts: func(t *testing.T) exchange.IdentityOptions { + t.Helper() + resolver := authkitmocks.NewPrincipalResolver(t) + resolver.EXPECT(). + ResolveIdentity(mock.Anything, mock.Anything). + Return(nil, resolverErr) + return exchange.IdentityOptions{ + Resolver: resolver, + AccessTokens: fakeAccessTokenIssuer{}, + } }, want: resolverErr, }, { name: "issuer failure", - opts: exchange.IdentityOptions{ - Resolver: fakeIdentityResolver{ - principal: testExchangePrincipal(), - }, - AccessTokens: fakeAccessTokenIssuer{ - err: issuerErr, - }, + setupOpts: func(t *testing.T) exchange.IdentityOptions { + t.Helper() + resolver := authkitmocks.NewPrincipalResolver(t) + resolver.EXPECT(). + ResolveIdentity(mock.Anything, mock.Anything). + Return(&principal, nil) + return exchange.IdentityOptions{ + Resolver: resolver, + AccessTokens: fakeAccessTokenIssuer{err: issuerErr}, + } }, want: issuerErr, }, @@ -126,7 +139,7 @@ func TestIdentityExchangerWrapsInternalFailures(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - exchanger := newIdentityExchanger(t, tt.opts) + exchanger := newIdentityExchanger(t, tt.setupOpts(t)) result, err := exchanger.Exchange(context.Background(), exchange.IdentityRequest{ Identity: testExchangeIdentity(), @@ -141,7 +154,7 @@ func TestIdentityExchangerWrapsInternalFailures(t *testing.T) { func TestIdentityExchangerPassesThroughContextErrors(t *testing.T) { exchanger := newIdentityExchanger(t, exchange.IdentityOptions{ - Resolver: fakeIdentityResolver{}, + Resolver: authkitmocks.NewPrincipalResolver(t), AccessTokens: fakeAccessTokenIssuer{}, }) ctx, cancel := context.WithCancel(context.Background()) @@ -155,46 +168,3 @@ func TestIdentityExchangerPassesThroughContextErrors(t *testing.T) { require.NotErrorIs(t, err, authkit.ErrInternal) assert.Empty(t, result) } - -func newIdentityExchanger(t *testing.T, opts exchange.IdentityOptions) *exchange.IdentityExchanger { - t.Helper() - - exchanger, err := exchange.NewIdentityExchanger(opts) - require.NoError(t, err) - - return exchanger -} - -func testExchangeIdentity() authkit.Identity { - return authkit.Identity{ - Provider: "https://issuer.example", - Subject: "user-123", - Claims: map[string]any{ - "email": "ada@example.test", - }, - } -} - -func testExchangePrincipal() authkit.Principal { - return authkit.Principal{ - ID: testPrincipalID, - Kind: authkit.PrincipalKindUser, - DisplayName: "Ada Lovelace", - } -} - -type fakeIdentityResolver struct { - principal authkit.Principal - err error -} - -func (f fakeIdentityResolver) ResolveIdentity( - context.Context, - authkit.Identity, -) (*authkit.Principal, error) { - if f.err != nil { - return nil, f.err - } - - return &f.principal, nil -} diff --git a/exchange/tokens.go b/exchange/tokens.go new file mode 100644 index 0000000..bdbc335 --- /dev/null +++ b/exchange/tokens.go @@ -0,0 +1,15 @@ +package exchange + +import ( + "context" + + "github.com/meigma/authkit/access/jwt" +) + +// AccessTokenIssuer issues short-lived authkit access JWTs bound to a principal. Implementations +// own signing, the kid header, and expiry policy; req.PrincipalID is required. The returned +// IssuedToken carries both the JWT string and its parsed metadata. +type AccessTokenIssuer interface { + // IssueToken issues an access JWT for req.PrincipalID. + IssueToken(ctx context.Context, req jwt.IssueRequest) (jwt.IssuedToken, error) +} diff --git a/exchange/types.go b/exchange/types.go deleted file mode 100644 index 074e762..0000000 --- a/exchange/types.go +++ /dev/null @@ -1,78 +0,0 @@ -package exchange - -import ( - "context" - - "github.com/meigma/authkit" - "github.com/meigma/authkit/access/jwt" - "github.com/meigma/authkit/proof/apikey" -) - -// APITokenVerifier verifies opaque API tokens. -type APITokenVerifier interface { - // VerifyAPIToken verifies plaintext and returns its authenticated token metadata. - VerifyAPIToken(ctx context.Context, plaintext string) (apikey.VerifiedToken, error) -} - -// AccessTokenIssuer issues authkit access JWTs. -type AccessTokenIssuer interface { - // IssueToken issues an access JWT for req.PrincipalID. - IssueToken(ctx context.Context, req jwt.IssueRequest) (jwt.IssuedToken, error) -} - -// APITokenOptions configures an APITokenExchanger. -type APITokenOptions struct { - // APITokens verifies opaque API tokens. - APITokens APITokenVerifier - - // Principals loads principals authenticated by API tokens. - Principals authkit.PrincipalFinder - - // AccessTokens issues authkit access JWTs. - AccessTokens AccessTokenIssuer -} - -// APITokenRequest describes an API-token exchange request. -type APITokenRequest struct { - // Plaintext is the opaque API token presented for exchange. - Plaintext string -} - -// APITokenResult describes a completed API-token exchange. -type APITokenResult struct { - // APIToken is the verified opaque API token metadata. - APIToken apikey.VerifiedToken - - // Principal is the principal authenticated by APIToken. - Principal authkit.Principal - - // AccessToken is the authkit access JWT issued for Principal. - AccessToken jwt.IssuedToken -} - -// IdentityOptions configures an IdentityExchanger. -type IdentityOptions struct { - // Resolver resolves or provisions verified external identities. - Resolver authkit.PrincipalResolver - - // AccessTokens issues authkit access JWTs. - AccessTokens AccessTokenIssuer -} - -// IdentityRequest describes an identity exchange request. -type IdentityRequest struct { - // Identity is the verified external identity presented for exchange. - Identity authkit.Identity -} - -// IdentityResult describes a completed identity exchange. -type IdentityResult struct { - // Identity is the verified external identity exchanged for an access JWT. - Identity authkit.Identity - - // Principal is the principal resolved or provisioned for Identity. - Principal authkit.Principal - - // AccessToken is the authkit access JWT issued for Principal. - AccessToken jwt.IssuedToken -} diff --git a/mocks/authkit/principal_resolver.go b/mocks/authkit/principal_resolver.go new file mode 100644 index 0000000..cee154b --- /dev/null +++ b/mocks/authkit/principal_resolver.go @@ -0,0 +1,107 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package authkitmocks + +import ( + "context" + + "github.com/meigma/authkit" + mock "github.com/stretchr/testify/mock" +) + +// NewPrincipalResolver creates a new instance of PrincipalResolver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPrincipalResolver(t interface { + mock.TestingT + Cleanup(func()) +}) *PrincipalResolver { + mock := &PrincipalResolver{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// PrincipalResolver is an autogenerated mock type for the PrincipalResolver type +type PrincipalResolver struct { + mock.Mock +} + +type PrincipalResolver_Expecter struct { + mock *mock.Mock +} + +func (_m *PrincipalResolver) EXPECT() *PrincipalResolver_Expecter { + return &PrincipalResolver_Expecter{mock: &_m.Mock} +} + +// ResolveIdentity provides a mock function for the type PrincipalResolver +func (_mock *PrincipalResolver) ResolveIdentity(ctx context.Context, identity authkit.Identity) (*authkit.Principal, error) { + ret := _mock.Called(ctx, identity) + + if len(ret) == 0 { + panic("no return value specified for ResolveIdentity") + } + + var r0 *authkit.Principal + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, authkit.Identity) (*authkit.Principal, error)); ok { + return returnFunc(ctx, identity) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, authkit.Identity) *authkit.Principal); ok { + r0 = returnFunc(ctx, identity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*authkit.Principal) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, authkit.Identity) error); ok { + r1 = returnFunc(ctx, identity) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrincipalResolver_ResolveIdentity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResolveIdentity' +type PrincipalResolver_ResolveIdentity_Call struct { + *mock.Call +} + +// ResolveIdentity is a helper method to define mock.On call +// - ctx context.Context +// - identity authkit.Identity +func (_e *PrincipalResolver_Expecter) ResolveIdentity(ctx interface{}, identity interface{}) *PrincipalResolver_ResolveIdentity_Call { + return &PrincipalResolver_ResolveIdentity_Call{Call: _e.mock.On("ResolveIdentity", ctx, identity)} +} + +func (_c *PrincipalResolver_ResolveIdentity_Call) Run(run func(ctx context.Context, identity authkit.Identity)) *PrincipalResolver_ResolveIdentity_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 authkit.Identity + if args[1] != nil { + arg1 = args[1].(authkit.Identity) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *PrincipalResolver_ResolveIdentity_Call) Return(principal *authkit.Principal, err error) *PrincipalResolver_ResolveIdentity_Call { + _c.Call.Return(principal, err) + return _c +} + +func (_c *PrincipalResolver_ResolveIdentity_Call) RunAndReturn(run func(ctx context.Context, identity authkit.Identity) (*authkit.Principal, error)) *PrincipalResolver_ResolveIdentity_Call { + _c.Call.Return(run) + return _c +}