Skip to content

Commit e366231

Browse files
committed
is: Reject access tokens and refreshes past session expiry
1 parent 8b18a97 commit e366231

4 files changed

Lines changed: 71 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ For details about compatibility between different releases, see the **Commitment
1313

1414
- Add tracing for LBS LNS and TTIGW protocol handlers.
1515
- TTGC LBS Root CUPS claiming support.
16-
- Configurable Identity Server user login session TTL via `is.user-login.session-ttl`. Defaults to `0` (no expiry, matching previous behavior). When set, the auth cookie becomes persistent with a matching `Max-Age`.
16+
- Configurable Identity Server user login session TTL via `is.user-login.session-ttl`. Defaults to `0` (no expiry, matching previous behavior). When set, the auth cookie becomes persistent with a matching `Max-Age`, and OAuth access tokens linked to a session are rejected once the session has expired — covering the Console API path.
1717

1818
### Changed
1919

pkg/identityserver/entity_access.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ func (is *IdentityServer) authInfo(ctx context.Context) (info *ttnpb.AuthInfoRes
197197
if expiresAt := ttnpb.StdTime(accessToken.ExpiresAt); expiresAt != nil && expiresAt.Before(time.Now()) {
198198
return errTokenExpired.New()
199199
}
200+
if accessToken.UserSessionId != "" {
201+
session, err := st.GetSession(ctx, accessToken.UserIds, accessToken.UserSessionId)
202+
if err != nil {
203+
if errors.IsNotFound(err) {
204+
return errTokenExpired.WithCause(err)
205+
}
206+
return err
207+
}
208+
if expiresAt := ttnpb.StdTime(session.ExpiresAt); expiresAt != nil && expiresAt.Before(time.Now()) {
209+
return errTokenExpired.New()
210+
}
211+
}
200212
accessToken.AccessToken, accessToken.RefreshToken = "", ""
201213
accessToken.Rights = ttnpb.RightsFrom(accessToken.Rights...).Implied().GetRights()
202214
res.AccessMethod = &ttnpb.AuthInfoResponse_OauthAccessToken{

pkg/oauth/server_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,7 @@ func TestTokenExchange(t *testing.T) {
753753
Name: "Exchange Refresh Token",
754754
StoreSetup: func(s *mockStore) {
755755
s.res.client = mockClient
756+
s.res.session = mockSession
756757
s.res.accessToken = &ttnpb.OAuthAccessToken{
757758
UserIds: mockUser.GetIds(),
758759
ClientIds: mockClient.GetIds(),
@@ -788,6 +789,43 @@ func TestTokenExchange(t *testing.T) {
788789
a.So(s.req.previousID, should.Equal, "IBTFXELDVVT64Y26IZZFFNSL7GWZY2Y3ALQQI3A")
789790
},
790791
},
792+
{
793+
Name: "Exchange Refresh Token with Expired Session",
794+
StoreSetup: func(s *mockStore) {
795+
s.res.client = mockClient
796+
s.res.session = &ttnpb.UserSession{
797+
UserIds: mockUser.GetIds(),
798+
SessionId: mockSession.SessionId,
799+
CreatedAt: timestamppb.New(now.Add(-2 * time.Hour)),
800+
ExpiresAt: timestamppb.New(now.Add(-time.Hour)),
801+
}
802+
s.res.accessToken = &ttnpb.OAuthAccessToken{
803+
UserIds: mockUser.GetIds(),
804+
ClientIds: mockClient.GetIds(),
805+
UserSessionId: mockSession.SessionId,
806+
Id: "SFUBFRKYTGULGPAXXM4SHIBYMKCPTIMQBM63ZGQ",
807+
RefreshToken: "PBKDF2$sha256$20000$IGAiKs46xX_M64E5$4xpyqnQT8SOa_Vf4xhEPk6WOZnhmAjG2mqGQiYBhm2s",
808+
Rights: mockClient.Rights,
809+
CreatedAt: timestamppb.New(now),
810+
ExpiresAt: timestamppb.New(anHourFromNow),
811+
}
812+
},
813+
Method: "POST",
814+
Path: "/oauth/token",
815+
Body: map[string]string{
816+
"grant_type": "refresh_token",
817+
"refresh_token": "OJSWM.IBTFXELDVVT64Y26IZZFFNSL7GWZY2Y3ALQQI3A.GCPIASDUP7UZJ6YL5OP2ESZB7CKRFV4JJQYTMDOSDIOE7O75IAMQ",
818+
"client_id": "client",
819+
"client_secret": "secret",
820+
},
821+
ExpectedCode: http.StatusUnauthorized,
822+
StoreCheck: func(t *testing.T, s *mockStore) {
823+
a := assertions.New(t)
824+
a.So(s.calls, should.Contain, "GetAccessToken")
825+
a.So(s.calls, should.Contain, "GetSession")
826+
a.So(s.calls, should.NotContain, "CreateAccessToken")
827+
},
828+
},
791829
} {
792830
name := tt.Name
793831
if name == "" {

pkg/oauth/storage.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ var errNoRefreshToken = errors.DefineInvalidArgument(
179179

180180
var errInvalidToken = errors.DefineInvalidArgument("token", "invalid token")
181181

182+
var errSessionExpired = errors.DefineUnauthenticated("session_expired", "session expired")
183+
182184
func (s *storage) SaveAccess(data *osin.AccessData) error {
183185
var accessHash, refreshHash string
184186
tokenType, accessID, accessKey, err := auth.SplitToken(data.AccessToken)
@@ -313,6 +315,24 @@ func (s *storage) LoadRefresh(token string) (*osin.AccessData, error) {
313315
if !valid || err != nil {
314316
return nil, errInvalidToken.New()
315317
}
318+
if userSessionIDs := data.UserData.(userData).UserSessionIdentifiers; userSessionIDs.GetSessionId() != "" {
319+
err = s.store.Transact(s.ctx, func(ctx context.Context, st oauth_store.Interface) error {
320+
session, err := st.GetSession(ctx, userSessionIDs.GetUserIds(), userSessionIDs.GetSessionId())
321+
if err != nil {
322+
if errors.IsNotFound(err) {
323+
return errSessionExpired.WithCause(err)
324+
}
325+
return err
326+
}
327+
if expiresAt := ttnpb.StdTime(session.ExpiresAt); expiresAt != nil && expiresAt.Before(time.Now()) {
328+
return errSessionExpired.New()
329+
}
330+
return nil
331+
})
332+
if err != nil {
333+
return nil, err
334+
}
335+
}
316336
return data, nil
317337
}
318338

0 commit comments

Comments
 (0)