Skip to content

Commit ec1a84e

Browse files
committed
Added RequestAuthorizationCodeViaAuthorizationCodeGrant command
1 parent 726c368 commit ec1a84e

File tree

6 files changed

+244
-7
lines changed

6 files changed

+244
-7
lines changed

authorization_command_handler.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@ package goauth2
22

33
import (
44
"github.com/inklabs/rangedb"
5+
"github.com/inklabs/rangedb/pkg/clock"
56
)
67

78
type authorizationCommandHandler struct {
89
store rangedb.Store
910
pendingEvents []rangedb.Event
1011
tokenGenerator TokenGenerator
12+
clock clock.Clock
1113
}
1214

13-
func newAuthorizationCommandHandler(store rangedb.Store, tokenGenerator TokenGenerator) *authorizationCommandHandler {
15+
func newAuthorizationCommandHandler(
16+
store rangedb.Store,
17+
tokenGenerator TokenGenerator,
18+
clock clock.Clock,
19+
) *authorizationCommandHandler {
1420
return &authorizationCommandHandler{
1521
store: store,
1622
tokenGenerator: tokenGenerator,
23+
clock: clock,
1724
}
1825
}
1926

@@ -135,6 +142,26 @@ func (h *authorizationCommandHandler) Handle(command Command) bool {
135142
return false
136143
}
137144

145+
case RequestAuthorizationCodeViaAuthorizationCodeGrant:
146+
clientApplication := h.loadClientApplicationAggregate(c.ClientID)
147+
148+
if !clientApplication.IsOnBoarded {
149+
h.emit(RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidClientApplicationID{
150+
UserID: c.UserID,
151+
ClientID: c.ClientID,
152+
})
153+
return false
154+
}
155+
156+
if clientApplication.RedirectUri != c.RedirectUri {
157+
h.emit(RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidClientApplicationRedirectUri{
158+
UserID: c.UserID,
159+
ClientID: c.ClientID,
160+
RedirectUri: c.RedirectUri,
161+
})
162+
return false
163+
}
164+
138165
}
139166

140167
return true
@@ -145,7 +172,7 @@ func (h *authorizationCommandHandler) emit(events ...rangedb.Event) {
145172
}
146173

147174
func (h *authorizationCommandHandler) loadResourceOwnerAggregate(userID string) *resourceOwner {
148-
return newResourceOwner(h.store.AllEventsByStream(resourceOwnerStream(userID)), h.tokenGenerator)
175+
return newResourceOwner(h.store.AllEventsByStream(resourceOwnerStream(userID)), h.tokenGenerator, h.clock)
149176
}
150177

151178
func (h *authorizationCommandHandler) loadClientApplicationAggregate(clientID string) *clientApplication {

goauth2.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@ package goauth2
22

33
import (
44
"github.com/inklabs/rangedb"
5+
"github.com/inklabs/rangedb/pkg/clock"
56
"github.com/inklabs/rangedb/provider/inmemorystore"
67

78
"github.com/inklabs/goauth2/provider/uuidtoken"
89
)
910

1011
//App is the OAuth2 CQRS application.
1112
type App struct {
13+
clock clock.Clock
1214
store rangedb.Store
13-
preCommandHandlers []PreCommandHandler
1415
tokenGenerator TokenGenerator
16+
preCommandHandlers []PreCommandHandler
1517
}
1618

1719
// Option defines functional option parameters for App.
1820
type Option func(*App)
1921

22+
//WithClock is a functional option to inject a clock.
23+
func WithClock(clock clock.Clock) Option {
24+
return func(app *App) {
25+
app.clock = clock
26+
}
27+
}
28+
2029
//WithStore is a functional option to inject a RangeDB Event Store.
2130
func WithStore(store rangedb.Store) Option {
2231
return func(app *App) {
@@ -43,7 +52,7 @@ func New(options ...Option) *App {
4352
}
4453

4554
app.preCommandHandlers = []PreCommandHandler{
46-
newAuthorizationCommandHandler(app.store, app.tokenGenerator),
55+
newAuthorizationCommandHandler(app.store, app.tokenGenerator, app.clock),
4756
}
4857

4958
return app
@@ -86,6 +95,9 @@ func (a *App) Dispatch(command Command) []rangedb.Event {
8695
case RequestAccessTokenViaRefreshTokenGrant:
8796
events = a.handleWithRefreshTokenAggregate(command)
8897

98+
case RequestAuthorizationCodeViaAuthorizationCodeGrant:
99+
events = a.handleWithResourceOwnerAggregate(command)
100+
89101
}
90102

91103
return events
@@ -98,13 +110,20 @@ func (a *App) handleWithClientApplicationAggregate(command Command) []rangedb.Ev
98110
}
99111

100112
func (a *App) handleWithResourceOwnerAggregate(command Command) []rangedb.Event {
101-
aggregate := newResourceOwner(a.store.AllEventsByStream(rangedb.GetEventStream(command)), a.tokenGenerator)
113+
aggregate := newResourceOwner(
114+
a.store.AllEventsByStream(rangedb.GetEventStream(command)),
115+
a.tokenGenerator,
116+
a.clock,
117+
)
102118
aggregate.Handle(command)
103119
return a.savePendingEvents(aggregate)
104120
}
105121

106122
func (a *App) handleWithRefreshTokenAggregate(command Command) []rangedb.Event {
107-
aggregate := newRefreshToken(a.store.AllEventsByStream(rangedb.GetEventStream(command)), a.tokenGenerator)
123+
aggregate := newRefreshToken(
124+
a.store.AllEventsByStream(rangedb.GetEventStream(command)),
125+
a.tokenGenerator,
126+
)
108127
aggregate.Handle(command)
109128
return a.savePendingEvents(aggregate)
110129
}

goauth2_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package goauth2_test
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/inklabs/rangedb"
8+
"github.com/inklabs/rangedb/pkg/clock/provider/seededclock"
79
"github.com/stretchr/testify/assert"
810
"github.com/stretchr/testify/require"
911

@@ -25,6 +27,12 @@ const (
2527
passwordHash = "$2a$10$U6ej0p2d9Y8OO2635R7l/O4oEBvxgc9o6gCaQ1wjMZ77dr4qGl8nu"
2628
password = "Pass123!"
2729
refreshToken = "1eff35434eee448884a2d7e2dd28b119"
30+
authorizationCode = "afa410b917034f67b64ec9164bf4140d"
31+
)
32+
33+
var (
34+
issueTime = time.Date(2020, 04, 1, 8, 0, 0, 0, time.UTC)
35+
issueTimePlus10Minutes = issueTime.Add(10 * time.Minute)
2836
)
2937

3038
func Test_OnBoardUser(t *testing.T) {
@@ -808,3 +816,117 @@ func Test_RequestAccessTokenViaRefreshTokenGrant_For_ClientApplication(t *testin
808816
},
809817
))
810818
}
819+
820+
func Test_RequestAuthorizationCodeViaAuthorizationCodeGrant(t *testing.T) {
821+
tokenGenerator := goauth2test.NewSeededTokenGenerator(authorizationCode)
822+
options := []goauth2.Option{
823+
goauth2.WithTokenGenerator(tokenGenerator),
824+
goauth2.WithClock(seededclock.New(issueTime)),
825+
}
826+
827+
t.Run("issues authorization code to user", goauth2TestCase(options...).
828+
Given(
829+
goauth2.UserWasOnBoarded{
830+
UserID: userID,
831+
Username: email,
832+
PasswordHash: passwordHash,
833+
},
834+
goauth2.ClientApplicationWasOnBoarded{
835+
ClientID: clientID,
836+
ClientSecret: clientSecret,
837+
RedirectUri: redirectUri,
838+
UserID: adminUserID,
839+
},
840+
).
841+
When(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrant{
842+
UserID: userID,
843+
ClientID: clientID,
844+
RedirectUri: redirectUri,
845+
Username: email,
846+
Password: password,
847+
}).
848+
Then(goauth2.AuthorizationCodeWasIssuedToUserViaAuthorizationCodeGrant{
849+
UserID: userID,
850+
AuthorizationCode: authorizationCode,
851+
ExpiresAt: issueTimePlus10Minutes.Unix(),
852+
}))
853+
854+
t.Run("rejected due to missing client application id", goauth2TestCase().
855+
Given().
856+
When(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrant{
857+
UserID: userID,
858+
ClientID: clientID,
859+
RedirectUri: redirectUri,
860+
Username: email,
861+
Password: password,
862+
}).
863+
Then(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidClientApplicationID{
864+
UserID: userID,
865+
ClientID: clientID,
866+
}))
867+
868+
t.Run("rejected due to invalid client application redirect uri", goauth2TestCase().
869+
Given(goauth2.ClientApplicationWasOnBoarded{
870+
ClientID: clientID,
871+
ClientSecret: clientSecret,
872+
RedirectUri: redirectUri,
873+
UserID: adminUserID,
874+
}).
875+
When(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrant{
876+
UserID: userID,
877+
ClientID: clientID,
878+
RedirectUri: wrongRedirectUri,
879+
Username: email,
880+
Password: password,
881+
}).
882+
Then(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidClientApplicationRedirectUri{
883+
UserID: userID,
884+
ClientID: clientID,
885+
RedirectUri: wrongRedirectUri,
886+
}))
887+
888+
t.Run("rejected due to missing user", goauth2TestCase().
889+
Given(goauth2.ClientApplicationWasOnBoarded{
890+
ClientID: clientID,
891+
ClientSecret: clientSecret,
892+
RedirectUri: redirectUri,
893+
UserID: adminUserID,
894+
}).
895+
When(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrant{
896+
UserID: userID,
897+
ClientID: clientID,
898+
RedirectUri: redirectUri,
899+
Username: email,
900+
Password: password,
901+
}).
902+
Then(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidUser{
903+
UserID: userID,
904+
ClientID: clientID,
905+
}))
906+
907+
t.Run("rejected due to invalid user password", goauth2TestCase().
908+
Given(
909+
goauth2.UserWasOnBoarded{
910+
UserID: userID,
911+
Username: email,
912+
PasswordHash: passwordHash,
913+
},
914+
goauth2.ClientApplicationWasOnBoarded{
915+
ClientID: clientID,
916+
ClientSecret: clientSecret,
917+
RedirectUri: redirectUri,
918+
UserID: adminUserID,
919+
},
920+
).
921+
When(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrant{
922+
UserID: userID,
923+
ClientID: clientID,
924+
RedirectUri: redirectUri,
925+
Username: email,
926+
Password: "wrong-password",
927+
}).
928+
Then(goauth2.RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidUserPassword{
929+
UserID: userID,
930+
ClientID: clientID,
931+
}))
932+
}

resource_owner.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package goauth2
22

33
import (
4+
"time"
5+
46
"github.com/inklabs/rangedb"
7+
"github.com/inklabs/rangedb/pkg/clock"
58

69
"github.com/inklabs/goauth2/pkg/securepass"
710
)
811

12+
const authorizationCodeLifetime = 10 * time.Minute
13+
914
type resourceOwner struct {
1015
IsOnBoarded bool
1116
Username string
@@ -14,11 +19,13 @@ type resourceOwner struct {
1419
IsAdministrator bool
1520
IsAuthorizedToOnboardClientApplications bool
1621
tokenGenerator TokenGenerator
22+
clock clock.Clock
1723
}
1824

19-
func newResourceOwner(records <-chan *rangedb.Record, tokenGenerator TokenGenerator) *resourceOwner {
25+
func newResourceOwner(records <-chan *rangedb.Record, tokenGenerator TokenGenerator, clock clock.Clock) *resourceOwner {
2026
aggregate := &resourceOwner{
2127
tokenGenerator: tokenGenerator,
28+
clock: clock,
2229
}
2330

2431
for record := range records {
@@ -160,6 +167,37 @@ func (a *resourceOwner) Handle(command Command) {
160167
},
161168
)
162169

170+
case RequestAuthorizationCodeViaAuthorizationCodeGrant:
171+
if !a.IsOnBoarded {
172+
a.emit(RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidUser{
173+
UserID: c.UserID,
174+
ClientID: c.ClientID,
175+
})
176+
return
177+
}
178+
179+
if !a.IsPasswordValid(c.Password) {
180+
a.emit(RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidUserPassword{
181+
UserID: c.UserID,
182+
ClientID: c.ClientID,
183+
})
184+
return
185+
}
186+
187+
authorizationCode, err := a.tokenGenerator.New()
188+
if err != nil {
189+
// TODO: emit error
190+
return
191+
}
192+
193+
expiresAt := a.clock.Now().Add(authorizationCodeLifetime).Unix()
194+
195+
a.emit(AuthorizationCodeWasIssuedToUserViaAuthorizationCodeGrant{
196+
UserID: c.UserID,
197+
AuthorizationCode: authorizationCode,
198+
ExpiresAt: expiresAt,
199+
})
200+
163201
}
164202
}
165203

resource_owner_commands.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ type RequestAccessTokenViaROPCGrant struct {
2929
Username string `json:"username"`
3030
Password string `json:"password"`
3131
}
32+
type RequestAuthorizationCodeViaAuthorizationCodeGrant struct {
33+
UserID string `json:"userID"`
34+
ClientID string `json:"clientID"`
35+
RedirectUri string `json:"redirectUri"`
36+
Username string `json:"username"`
37+
Password string `json:"password"`
38+
}

resource_owner_events.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,27 @@ type RequestAccessTokenViaROPCGrantWasRejectedDueToInvalidClientApplicationCrede
9696
UserID string `json:"userID"`
9797
ClientID string `json:"clientID"`
9898
}
99+
100+
// RequestAuthorizationCodeViaAuthorizationCodeGrant Events
101+
type AuthorizationCodeWasIssuedToUserViaAuthorizationCodeGrant struct {
102+
UserID string `json:"userID"`
103+
AuthorizationCode string `json:"authorizationCode"`
104+
ExpiresAt int64 `json:"expiresAt"`
105+
}
106+
type RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidClientApplicationID struct {
107+
UserID string `json:"userID"`
108+
ClientID string `json:"clientID"`
109+
}
110+
type RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidClientApplicationRedirectUri struct {
111+
UserID string `json:"userID"`
112+
ClientID string `json:"clientID"`
113+
RedirectUri string `json:"redirectUri"`
114+
}
115+
type RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidUser struct {
116+
UserID string `json:"userID"`
117+
ClientID string `json:"clientID"`
118+
}
119+
type RequestAuthorizationCodeViaAuthorizationCodeGrantWasRejectedDueToInvalidUserPassword struct {
120+
UserID string `json:"userID"`
121+
ClientID string `json:"clientID"`
122+
}

0 commit comments

Comments
 (0)