This repository has been archived by the owner on Mar 11, 2021. It is now read-only.
/
token_manager.go
1252 lines (1096 loc) · 44.6 KB
/
token_manager.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package manager
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/fabric8-services/fabric8-auth/authentication/account"
"github.com/fabric8-services/fabric8-auth/authentication/account/repository"
"github.com/fabric8-services/fabric8-auth/authorization/token"
authclient "github.com/fabric8-services/fabric8-auth/client"
autherrors "github.com/fabric8-services/fabric8-auth/errors"
"github.com/fabric8-services/fabric8-auth/goasupport"
"github.com/fabric8-services/fabric8-auth/log"
"github.com/fabric8-services/fabric8-auth/rest"
jwt "github.com/dgrijalva/jwt-go"
"github.com/goadesign/goa"
"github.com/goadesign/goa/client"
goajwt "github.com/goadesign/goa/middleware/security/jwt"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"golang.org/x/oauth2"
jose "gopkg.in/square/go-jose.v2"
)
var defaultManager TokenManager
var defaultOnce sync.Once
var defaultErr error
const (
//contextTokenManagerKey is a key that will be used to put and to get `tokenManager` from goa.context
contextTokenManagerKey = iota
)
// DefaultManager creates the default manager if it has not created yet.
// This function must be called in main to make sure the default manager is created during service startup.
// It will try to create the default manager only once even if called multiple times.
func DefaultManager(config TokenManagerConfiguration) (TokenManager, error) {
defaultOnce.Do(func() {
defaultManager, defaultErr = NewTokenManager(config)
})
return defaultManager, defaultErr
}
// TokenManagerConfiguration represents configuration needed to construct a token manager
type TokenManagerConfiguration interface {
GetServiceAccountPrivateKey() ([]byte, string)
GetDeprecatedServiceAccountPrivateKey() ([]byte, string)
GetUserAccountPrivateKey() ([]byte, string)
GetDeprecatedUserAccountPrivateKey() ([]byte, string)
GetDevModePublicKey() (bool, []byte, string)
IsPostgresDeveloperModeEnabled() bool
GetAccessTokenExpiresIn() int64
GetRefreshTokenExpiresIn() int64
GetTransientTokenExpiresIn() int64
GetAuthServiceURL() string
}
// TokenClaims represents access token claims
type TokenClaims struct {
Name string `json:"name"`
Username string `json:"preferred_username"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Company string `json:"company"`
SessionState string `json:"session_state"`
Approved bool `json:"approved"`
Permissions *[]Permissions `json:"permissions"`
jwt.StandardClaims
}
// Permissions represents a "permissions" claim in the AuthorizationPayload
type Permissions struct {
ResourceSetName *string `json:"resource_set_name"`
ResourceSetID *string `json:"resource_set_id"`
Scopes []string `json:"scopes"`
Expiry int64 `json:"exp"`
}
// #####################################################################################################################
//
// Token sets
//
// #####################################################################################################################
// TokenSet represents a set of Access and Refresh tokens
type TokenSet struct {
AccessToken *string `json:"access_token,omitempty"`
ExpiresIn *int64 `json:"expires_in,omitempty"`
NotBeforePolicy *int64 `json:"not-before-policy,omitempty"`
RefreshExpiresIn *int64 `json:"refresh_expires_in,omitempty"`
RefreshToken *string `json:"refresh_token,omitempty"`
TokenType *string `json:"token_type,omitempty"`
}
// ReadTokenSetFromJson parses json with a token set
func ReadTokenSetFromJson(ctx context.Context, jsonString string) (*TokenSet, error) {
var token TokenSet
err := json.Unmarshal([]byte(jsonString), &token)
if err != nil {
return nil, errors.Wrapf(err, "error when unmarshal json with access token %s ", jsonString)
}
return &token, nil
}
// #####################################################################################################################
//
// Context management
//
// #####################################################################################################################
// ContextIdentity returns the identity's ID found in given context
// Uses tokenManager.Locate to fetch the identity of currently logged in user
func ContextIdentity(ctx context.Context) (*uuid.UUID, error) {
tm, err := ReadTokenManagerFromContext(ctx)
if err != nil {
log.Error(ctx, map[string]interface{}{}, "error reading token manager")
return nil, errors.Wrapf(err, "error reading token manager")
}
// As mentioned in token.go, we can now safely convert tm to a token.Manager
manager := tm.(TokenManager)
uuid, err := manager.Locate(ctx)
if err != nil {
// TODO : need a way to define user as Guest
log.Error(ctx, map[string]interface{}{
"uuid": uuid,
"err": err,
}, "identity belongs to a Guest User")
return nil, errors.WithStack(err)
}
return &uuid, nil
}
// ContextWithTokenManager injects tokenManager in the context for every incoming request
// Accepts Token.Manager in order to make sure that correct object is set in the context.
// Only other possible value is nil
func ContextWithTokenManager(ctx context.Context, tm interface{}) context.Context {
return context.WithValue(ctx, contextTokenManagerKey, tm)
}
// ReadTokenManagerFromContext extracts the token manager from the context and returns it
func ReadTokenManagerFromContext(ctx context.Context) (TokenManager, error) {
tm := ctx.Value(contextTokenManagerKey)
if tm == nil {
log.Error(ctx, map[string]interface{}{
"token": tm,
}, "missing token manager")
return nil, errors.New("missing token manager")
}
return tm.(*tokenManager), nil
}
// InjectTokenManager is a middleware responsible for setting up tokenManager in the context for every request.
func InjectTokenManager(tokenManager TokenManager) goa.Middleware {
return func(h goa.Handler) goa.Handler {
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
ctxWithTM := ContextWithTokenManager(ctx, tokenManager)
return h(ctxWithTM, rw, req)
}
}
}
// #####################################################################################################################
//
// Token Manager types and constructor
//
// #####################################################################################################################
// TokenManager generates and manages auth tokens
type TokenManager interface {
Parse(ctx context.Context, tokenString string) (*jwt.Token, error)
PublicKeys() []*rsa.PublicKey
Locate(ctx context.Context) (uuid.UUID, error)
ParseToken(ctx context.Context, tokenString string) (*TokenClaims, error)
ParseTokenWithMapClaims(ctx context.Context, tokenString string) (jwt.MapClaims, error)
PublicKey(keyID string) *rsa.PublicKey
JSONWebKeys() token.JSONKeys
PemKeys() token.JSONKeys
KeyFunction(context.Context) jwt.Keyfunc
AuthServiceAccountToken() string
GenerateServiceAccountToken(saID string, saName string) (string, error)
GenerateUnsignedServiceAccountToken(saID string, saName string) *jwt.Token
GenerateUserTokenForAPIClient(ctx context.Context, providerToken oauth2.Token) (*oauth2.Token, error)
GenerateUserTokenForIdentity(ctx context.Context, identity repository.Identity, offlineToken bool) (*oauth2.Token, error)
GenerateTransientUserAccessTokenForIdentity(ctx context.Context, identity repository.Identity) (*string, error)
GenerateUserTokenUsingRefreshToken(ctx context.Context, refreshTokenString string, identity *repository.Identity, permissions []Permissions) (*oauth2.Token, error)
GenerateUnsignedRPTTokenForIdentity(ctx context.Context, tokenClaims *TokenClaims, identity repository.Identity, permissions *[]Permissions) (*jwt.Token, error)
SignRPTToken(ctx context.Context, rptToken *jwt.Token) (string, error)
ConvertTokenSet(tokenSet TokenSet) *oauth2.Token
ConvertToken(oauthToken oauth2.Token) (*TokenSet, error)
AddLoginRequiredHeaderToUnauthorizedError(err error, rw http.ResponseWriter)
AddLoginRequiredHeader(rw http.ResponseWriter)
AuthServiceAccountSigner() client.Signer
}
type tokenManager struct {
publicKeysMap map[string]*rsa.PublicKey
publicKeys []*token.PublicKey
serviceAccountPrivateKey *token.PrivateKey
userAccountPrivateKey *token.PrivateKey
jsonWebKeys token.JSONKeys
pemKeys token.JSONKeys
serviceAccountToken string
config TokenManagerConfiguration
}
// NewTokenManager returns a new token Manager for handling tokens
func NewTokenManager(config TokenManagerConfiguration) (TokenManager, error) {
tm := &tokenManager{
publicKeysMap: map[string]*rsa.PublicKey{},
}
tm.config = config
// Load the user account private key and add it to the manager.
// Extract the public key from it and add it to the map of public keys.
var err error
key, kid := config.GetUserAccountPrivateKey()
deprecatedKey, deprecatedKid := config.GetDeprecatedUserAccountPrivateKey()
tm.userAccountPrivateKey, err = tm.loadPrivateKey(tm, key, kid, deprecatedKey, deprecatedKid)
if err != nil {
log.Error(nil, map[string]interface{}{"err": err}, "unable to load user account private keys")
return nil, err
}
// Load the service account private key and add it to the manager.
// Extract the public key from it and add it to the map of public keys.
key, kid = config.GetServiceAccountPrivateKey()
deprecatedKey, deprecatedKid = config.GetDeprecatedServiceAccountPrivateKey()
tm.serviceAccountPrivateKey, err = tm.loadPrivateKey(tm, key, kid, deprecatedKey, deprecatedKid)
if err != nil {
log.Error(nil, map[string]interface{}{"err": err}, "unable to load service account private keys")
return nil, err
}
// Load Keycloak public key if run in dev mode.
devMode, key, kid := config.GetDevModePublicKey()
if devMode {
rsaKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
if err != nil {
log.Error(nil, map[string]interface{}{"err": err}, "unable to load dev mode public key")
return nil, err
}
tm.publicKeysMap[kid] = rsaKey
tm.publicKeys = append(tm.publicKeys, &token.PublicKey{KeyID: kid, Key: rsaKey})
log.Info(nil, map[string]interface{}{"kid": kid}, "dev mode public key added")
}
// Convert public keys to JWK format
jsonWebKeys, err := tm.toJSONWebKeys(tm.publicKeys)
if err != nil {
log.Error(nil, map[string]interface{}{"err": err}, "unable to convert public keys to JSON Web Keys")
return nil, errors.New("unable to convert public keys to JSON Web Keys")
}
tm.jsonWebKeys = jsonWebKeys
// Convert public keys to PEM format
jsonKeys, err := tm.toPemKeys(tm.publicKeys)
if err != nil {
log.Error(nil, map[string]interface{}{"err": err}, "unable to convert public keys to PEM Keys")
return nil, errors.New("unable to convert public keys to PEM Keys")
}
tm.pemKeys = jsonKeys
tm.initServiceAccountToken()
return tm, nil
}
// #####################################################################################################################
//
// Service Account functions (Service accounts are special non-user accounts used by other services)
//
// #####################################################################################################################
// GenerateServiceAccountToken generates and signs a new Service Account Token (Protection API Token)
func (m *tokenManager) GenerateServiceAccountToken(saID string, saName string) (string, error) {
token := m.GenerateUnsignedServiceAccountToken(saID, saName)
tokenStr, err := token.SignedString(m.serviceAccountPrivateKey.Key)
if err != nil {
return "", errors.WithStack(err)
}
return tokenStr, nil
}
// GenerateUnsignedServiceAccountToken generates an unsigned Service Account Token (Protection API Token)
func (m *tokenManager) GenerateUnsignedServiceAccountToken(saID string, saName string) *jwt.Token {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.serviceAccountPrivateKey.KeyID
claims := token.Claims.(jwt.MapClaims)
claims["service_accountname"] = saName
claims["sub"] = saID
claims["jti"] = uuid.NewV4().String()
claims["iat"] = time.Now().Unix()
claims["iss"] = m.config.GetAuthServiceURL()
claims["scopes"] = []string{"uma_protection"}
return token
}
// AuthServiceAccountSigner returns a new JWT signer which uses the Auth Service Account token
func (m *tokenManager) AuthServiceAccountSigner() client.Signer {
return &goasupport.JWTSigner{Token: m.AuthServiceAccountToken()}
}
// AuthServiceAccountToken returns the service account token which authenticates the Auth service
func (m *tokenManager) AuthServiceAccountToken() string {
return m.serviceAccountToken
}
// #####################################################################################################################
//
// User Token functions (User tokens are an oauth2 token consisting of an access token, refresh token and signature
//
// #####################################################################################################################
// GenerateUserTokenForIdentity generates an OAuth2 user token for the given identity
func (m *tokenManager) GenerateUserTokenForIdentity(ctx context.Context, identity repository.Identity, offlineToken bool) (*oauth2.Token, error) {
nowTime := time.Now().Unix()
unsignedAccessToken, err := m.GenerateUnsignedUserAccessTokenForIdentity(ctx, identity)
if err != nil {
return nil, errors.WithStack(err)
}
accessToken, err := unsignedAccessToken.SignedString(m.userAccountPrivateKey.Key)
if err != nil {
return nil, errors.WithStack(err)
}
unsignedRefreshToken, err := m.GenerateUnsignedUserRefreshTokenForIdentity(ctx, identity, offlineToken)
if err != nil {
return nil, errors.WithStack(err)
}
refreshToken, err := unsignedRefreshToken.SignedString(m.userAccountPrivateKey.Key)
if err != nil {
return nil, errors.WithStack(err)
}
var nbf int64
token := &oauth2.Token{
AccessToken: accessToken,
RefreshToken: refreshToken,
Expiry: time.Unix(nowTime+m.config.GetAccessTokenExpiresIn(), 0),
TokenType: "Bearer",
}
// Derivative OAuth2 claims "expires_in" and "refresh_expires_in"
extra := make(map[string]interface{})
extra["expires_in"] = m.config.GetAccessTokenExpiresIn()
extra["refresh_expires_in"] = m.config.GetRefreshTokenExpiresIn()
extra["not_before_policy"] = nbf
token = token.WithExtra(extra)
return token, nil
}
// #####################################################################################################################
//
// RPT Token functions (RPT tokens are an access token with an additional "permissions" claim
//
// #####################################################################################################################
// GenerateUnsignedRPTTokenForIdentity generates a JWT RPT token for the given identity and specified permissions.
func (m *tokenManager) GenerateUnsignedRPTTokenForIdentity(ctx context.Context, tokenClaims *TokenClaims, identity repository.Identity, permissions *[]Permissions) (*jwt.Token, error) {
unsignedRPTtoken, err := m.GenerateUnsignedUserAccessTokenFromClaims(ctx, tokenClaims, &identity)
if err != nil {
return nil, err
}
claims := unsignedRPTtoken.Claims.(jwt.MapClaims)
if permissions != nil && len(*permissions) > 0 {
claims["permissions"] = permissions
}
return unsignedRPTtoken, nil
}
// SignRPTToken generates a signature for the specified rpt token and returns it
func (mgm *tokenManager) SignRPTToken(ctx context.Context, rptToken *jwt.Token) (string, error) {
return rptToken.SignedString(mgm.userAccountPrivateKey.Key)
}
// #####################################################################################################################
//
// Access Token functions (Access tokens are trusted tokens used to identify a user)
//
// #####################################################################################################################
// GenerateUnsignedUserAccessTokenFromClaims generates a new token based on the specified claims
func (m *tokenManager) GenerateUnsignedUserAccessTokenFromClaims(ctx context.Context, tokenClaims *TokenClaims, identity *repository.Identity) (*jwt.Token, error) {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.userAccountPrivateKey.KeyID
claims := token.Claims.(jwt.MapClaims)
claims["jti"] = uuid.NewV4().String()
// TODO generate value instead of using it from claim
claims["exp"] = tokenClaims.ExpiresAt
claims["nbf"] = tokenClaims.NotBefore
claims["iat"] = tokenClaims.IssuedAt
claims["iss"] = tokenClaims.Issuer
claims["aud"] = tokenClaims.Audience
claims["typ"] = "Bearer"
claims["auth_time"] = tokenClaims.IssuedAt
claims["approved"] = identity != nil && !identity.User.Banned && tokenClaims.Approved
if identity != nil {
claims["sub"] = identity.ID.String()
claims["email_verified"] = identity.User.EmailVerified
claims["name"] = identity.User.FullName
claims["preferred_username"] = identity.Username
firstName, lastName := account.SplitFullName(identity.User.FullName)
claims["given_name"] = firstName
claims["family_name"] = lastName
claims["email"] = identity.User.Email
} else {
claims["sub"] = tokenClaims.Subject
claims["email_verified"] = tokenClaims.EmailVerified
claims["name"] = tokenClaims.Name
claims["preferred_username"] = tokenClaims.Username
claims["given_name"] = tokenClaims.GivenName
claims["family_name"] = tokenClaims.FamilyName
claims["email"] = tokenClaims.Email
}
authOpenshiftIO := m.config.GetAuthServiceURL()
openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURLStr(authOpenshiftIO, "", "")
if err != nil {
return nil, errors.WithStack(err)
}
claims["allowed-origins"] = []string{
authOpenshiftIO,
openshiftIO,
}
claims["azp"] = tokenClaims.Audience
claims["session_state"] = tokenClaims.SessionState
claims["acr"] = "0"
realmAccess := make(map[string]interface{})
realmAccess["roles"] = []string{"uma_authorization"}
claims["realm_access"] = realmAccess
resourceAccess := make(map[string]interface{})
broker := make(map[string]interface{})
broker["roles"] = []string{"read-token"}
resourceAccess["broker"] = broker
account := make(map[string]interface{})
account["roles"] = []string{"manage-account", "manage-account-links", "view-profile"}
resourceAccess["account"] = account
claims["resource_access"] = resourceAccess
claims["permissions"] = tokenClaims.Permissions
return token, nil
}
// GenerateUnsignedUserAccessTokenForIdentity generates an unsigned OAuth2 user access token for the given identity
func (m *tokenManager) GenerateUnsignedUserAccessTokenForIdentity(ctx context.Context, identity repository.Identity) (*jwt.Token, error) {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.userAccountPrivateKey.KeyID
authOpenshiftIO := m.config.GetAuthServiceURL()
openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURLStr(authOpenshiftIO, "", "")
if err != nil {
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
claims["jti"] = uuid.NewV4().String()
iat := time.Now().Unix()
claims["exp"] = iat + m.config.GetAccessTokenExpiresIn()
claims["nbf"] = 0
claims["iat"] = iat
claims["iss"] = authOpenshiftIO
claims["aud"] = openshiftIO
claims["typ"] = "Bearer"
claims["auth_time"] = iat // TODO should use the time when user actually logged-in the last time. Will need to get this time from the RHD token
claims["approved"] = !identity.User.Banned
claims["sub"] = identity.ID.String()
claims["email_verified"] = identity.User.EmailVerified
claims["name"] = identity.User.FullName
claims["preferred_username"] = identity.Username
firstName, lastName := account.SplitFullName(identity.User.FullName)
claims["given_name"] = firstName
claims["family_name"] = lastName
claims["email"] = identity.User.Email
claims["company"] = identity.User.Company
claims["allowed-origins"] = []string{
authOpenshiftIO,
openshiftIO,
}
claims["session_state"] = uuid.NewV4().String()
return token, nil
}
// GenerateTransientUserAccessTokenForIdentity generates a transient user access token, an extremely short-lived token
func (m *tokenManager) GenerateTransientUserAccessTokenForIdentity(ctx context.Context, identity repository.Identity) (*string, error) {
token, err := m.GenerateUnsignedUserAccessTokenForIdentity(ctx, identity)
if err != nil {
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
iat := time.Now().Unix()
claims["exp"] = iat + m.config.GetTransientTokenExpiresIn()
claims["transient"] = "true"
accessToken, err := token.SignedString(m.userAccountPrivateKey.Key)
if err != nil {
return nil, errors.WithStack(err)
}
return &accessToken, nil
}
// #####################################################################################################################
//
// Refresh token functions (refresh tokens are used to obtain a new user token)
//
// #####################################################################################################################
// GenerateUnsignedUserRefreshTokenForIdentity generates an unsigned OAuth2 user refresh token for the given identity
func (m *tokenManager) GenerateUnsignedUserRefreshTokenForIdentity(ctx context.Context, identity repository.Identity, offlineToken bool) (*jwt.Token, error) {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.userAccountPrivateKey.KeyID
authOpenshiftIO := m.config.GetAuthServiceURL()
openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURLStr(authOpenshiftIO, "", "")
if err != nil {
return nil, errors.WithStack(err)
}
claims := token.Claims.(jwt.MapClaims)
claims["jti"] = uuid.NewV4().String()
iat := time.Now().Unix()
var exp int64 // Offline tokens do not expire
typ := "Offline"
if !offlineToken {
exp = iat + m.config.GetRefreshTokenExpiresIn()
typ = "Refresh"
}
claims["exp"] = exp
claims["nbf"] = 0
claims["iat"] = iat
claims["iss"] = authOpenshiftIO
claims["aud"] = openshiftIO
claims["typ"] = typ
claims["auth_time"] = 0
claims["sub"] = identity.ID.String()
claims["session_state"] = uuid.NewV4().String()
return token, nil
}
// TODO combine this with GenerateUnsignedUserRefreshTokenForIdentity, make previous refreshToken parameter optional
// GenerateUnsignedUserRefreshToken generates an unsigned OAuth2 user refresh token for the given identity based on the provided refresh token
func (m *tokenManager) GenerateUnsignedUserRefreshToken(ctx context.Context, refreshToken string, identity *repository.Identity) (*jwt.Token, error) {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.userAccountPrivateKey.KeyID
oldClaims, err := m.ParseToken(ctx, refreshToken)
if err != nil {
return nil, errors.WithStack(err)
}
authOpenshiftIO := m.config.GetAuthServiceURL()
openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURLStr(authOpenshiftIO, "", "")
if err != nil {
return nil, errors.WithStack(err)
}
claims := token.Claims.(jwt.MapClaims)
claims["jti"] = uuid.NewV4().String()
iat := time.Now().Unix()
var exp int64 // Offline tokens do not expire
typ := "Offline"
if oldClaims.ExpiresAt != 0 {
exp = iat + m.config.GetRefreshTokenExpiresIn()
typ = "Refresh"
}
claims["exp"] = exp
claims["nbf"] = 0
claims["iat"] = iat
claims["iss"] = authOpenshiftIO
claims["aud"] = openshiftIO
claims["typ"] = typ
claims["auth_time"] = 0
if identity != nil {
claims["sub"] = identity.ID.String()
} else {
// populate claims for user details in refresh token for api_client as we don't have identity in db for it
claims["sub"] = oldClaims.Subject
claims["email_verified"] = oldClaims.EmailVerified
claims["name"] = oldClaims.Name
claims["preferred_username"] = oldClaims.Username
claims["given_name"] = oldClaims.GivenName
claims["family_name"] = oldClaims.FamilyName
claims["email"] = oldClaims.Email
}
claims["azp"] = oldClaims.Audience
claims["session_state"] = oldClaims.SessionState
return token, nil
}
// GenerateUnsignedUserAccessTokenFromRefreshToken
func (m *tokenManager) GenerateUnsignedUserAccessTokenFromRefreshToken(ctx context.Context, refreshTokenString string, identity *repository.Identity) (*jwt.Token, error) {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.userAccountPrivateKey.KeyID
authOpenshiftIO := m.config.GetAuthServiceURL()
openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURLStr(authOpenshiftIO, "", "")
if err != nil {
return nil, errors.WithStack(err)
}
refreshTokenClaims, err := m.ParseToken(ctx, refreshTokenString)
if err != nil {
return nil, errors.WithStack(err)
}
claims := token.Claims.(jwt.MapClaims)
claims["jti"] = uuid.NewV4().String()
iat := time.Now().Unix()
claims["exp"] = iat + m.config.GetAccessTokenExpiresIn()
claims["nbf"] = 0
claims["iat"] = iat
claims["iss"] = authOpenshiftIO
claims["aud"] = openshiftIO
claims["typ"] = "Bearer"
claims["auth_time"] = iat // TODO should use the time when user actually logged-in the last time. Will need to get this time from the RHD token
claims["allowed-origins"] = []string{
authOpenshiftIO,
openshiftIO,
}
claims["approved"] = identity != nil && !identity.User.Banned
if identity != nil {
claims["sub"] = identity.ID.String()
claims["email_verified"] = identity.User.EmailVerified
claims["name"] = identity.User.FullName
claims["preferred_username"] = identity.Username
firstName, lastName := account.SplitFullName(identity.User.FullName)
claims["given_name"] = firstName
claims["family_name"] = lastName
claims["email"] = identity.User.Email
claims["company"] = identity.User.Company
} else {
claims["sub"] = refreshTokenClaims.Subject
// refresh token should have all following claims included only for api_client(e.g. vscode analytics) who don't have identity in auth db
claims["email_verified"] = refreshTokenClaims.EmailVerified
claims["name"] = refreshTokenClaims.Name
claims["preferred_username"] = refreshTokenClaims.Username
claims["given_name"] = refreshTokenClaims.GivenName
claims["family_name"] = refreshTokenClaims.FamilyName
claims["email"] = refreshTokenClaims.Email
claims["company"] = refreshTokenClaims.Company
}
claims["azp"] = refreshTokenClaims.Audience
claims["session_state"] = refreshTokenClaims.SessionState
claims["acr"] = "0"
realmAccess := make(map[string]interface{})
realmAccess["roles"] = []string{"uma_authorization"}
claims["realm_access"] = realmAccess
resourceAccess := make(map[string]interface{})
broker := make(map[string]interface{})
broker["roles"] = []string{"read-token"}
resourceAccess["broker"] = broker
account := make(map[string]interface{})
account["roles"] = []string{"manage-account", "manage-account-links", "view-profile"}
resourceAccess["account"] = account
claims["resource_access"] = resourceAccess
return token, nil
}
// GenerateUserTokenUsingRefreshToken
func (m *tokenManager) GenerateUserTokenUsingRefreshToken(ctx context.Context, refreshTokenString string,
identity *repository.Identity, permissions []Permissions) (*oauth2.Token, error) {
nowTime := time.Now().Unix()
unsignedAccessToken, err := m.GenerateUnsignedUserAccessTokenFromRefreshToken(ctx, refreshTokenString, identity)
if err != nil {
return nil, errors.WithStack(err)
}
if permissions != nil && len(permissions) > 0 {
claims := unsignedAccessToken.Claims.(jwt.MapClaims)
claims["permissions"] = permissions
}
accessToken, err := unsignedAccessToken.SignedString(m.userAccountPrivateKey.Key)
if err != nil {
return nil, errors.WithStack(err)
}
unsignedRefreshToken, err := m.GenerateUnsignedUserRefreshToken(ctx, refreshTokenString, identity)
if err != nil {
return nil, errors.WithStack(err)
}
refreshToken, err := unsignedRefreshToken.SignedString(m.userAccountPrivateKey.Key)
if err != nil {
return nil, errors.WithStack(err)
}
var nbf int64
token := &oauth2.Token{
AccessToken: accessToken,
RefreshToken: refreshToken,
Expiry: time.Unix(nowTime+m.config.GetAccessTokenExpiresIn(), 0),
TokenType: "Bearer",
}
// Derivative OAuth2 claims "expires_in" and "refresh_expires_in"
extra := make(map[string]interface{})
extra["expires_in"] = m.config.GetAccessTokenExpiresIn()
extra["refresh_expires_in"] = m.config.GetRefreshTokenExpiresIn()
extra["not_before_policy"] = nbf
token = token.WithExtra(extra)
return token, nil
}
// #####################################################################################################################
//
// APIClient functions
//
// #####################################################################################################################
// GenerateUserTokenForAPIClient
func (m *tokenManager) GenerateUserTokenForAPIClient(ctx context.Context, providerToken oauth2.Token) (*oauth2.Token, error) {
unsignedAccessToken, err := m.GenerateUnsignedUserAccessTokenForAPIClient(ctx, providerToken.AccessToken)
if err != nil {
return nil, errors.WithStack(err)
}
accessToken, err := unsignedAccessToken.SignedString(m.userAccountPrivateKey.Key)
if err != nil {
return nil, errors.WithStack(err)
}
unsignedRefreshToken, err := m.GenerateUnsignedUserRefreshTokenForAPIClient(ctx, providerToken.AccessToken)
if err != nil {
return nil, errors.WithStack(err)
}
refreshToken, err := unsignedRefreshToken.SignedString(m.userAccountPrivateKey.Key)
if err != nil {
return nil, errors.WithStack(err)
}
token := &oauth2.Token{
AccessToken: accessToken,
RefreshToken: refreshToken,
Expiry: providerToken.Expiry,
TokenType: "Bearer",
}
// Derivative OAuth2 claims "expires_in" and "refresh_expires_in"
extra := make(map[string]interface{})
expiresIn := providerToken.Extra("expires_in")
if expiresIn != nil {
extra["expires_in"] = expiresIn
}
refreshExpiresIn := providerToken.Extra("refresh_expires_in")
if refreshExpiresIn != nil {
extra["refresh_expires_in"] = refreshExpiresIn
}
notBeforePolicy := providerToken.Extra("not_before_policy")
if notBeforePolicy != nil {
extra["not_before_policy"] = notBeforePolicy
}
if len(extra) > 0 {
token = token.WithExtra(extra)
}
return token, nil
}
// GenerateUnsignedUserAccessTokenForAPIClient generates an unsigned OAuth2 user access token for the api_client based on the Keycloak token
func (m *tokenManager) GenerateUnsignedUserAccessTokenForAPIClient(ctx context.Context, providerAccessToken string) (*jwt.Token, error) {
kcClaims, err := m.ParseToken(ctx, providerAccessToken)
if err != nil {
return nil, errors.WithStack(err)
}
return m.GenerateUnsignedUserAccessTokenFromClaimsForAPIClient(ctx, kcClaims)
}
// GenerateUnsignedUserAccessTokenFromClaimsForAPIClient generates a new token based on the specified claims for api_client
func (m *tokenManager) GenerateUnsignedUserAccessTokenFromClaimsForAPIClient(ctx context.Context, tokenClaims *TokenClaims) (*jwt.Token, error) {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.userAccountPrivateKey.KeyID
authOpenshiftIO := m.config.GetAuthServiceURL()
openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURLStr(authOpenshiftIO, "", "")
if err != nil {
return nil, errors.WithStack(err)
}
claims := token.Claims.(jwt.MapClaims)
claims["jti"] = uuid.NewV4().String()
iat := time.Now().Unix()
claims["exp"] = iat + m.config.GetAccessTokenExpiresIn()
claims["nbf"] = 0
claims["iat"] = iat
claims["iss"] = authOpenshiftIO
claims["aud"] = openshiftIO
claims["typ"] = "Bearer"
claims["auth_time"] = iat
claims["typ"] = "Bearer"
claims["approved"] = tokenClaims.Approved
claims["sub"] = tokenClaims.Subject
claims["email_verified"] = tokenClaims.EmailVerified
claims["name"] = tokenClaims.Name
claims["preferred_username"] = tokenClaims.Username
claims["given_name"] = tokenClaims.GivenName
claims["family_name"] = tokenClaims.FamilyName
claims["email"] = tokenClaims.Email
claims["allowed-origins"] = []string{
authOpenshiftIO,
openshiftIO,
}
claims["azp"] = tokenClaims.Audience
claims["session_state"] = tokenClaims.SessionState
claims["acr"] = "0"
realmAccess := make(map[string]interface{})
realmAccess["roles"] = []string{"uma_authorization"}
claims["realm_access"] = realmAccess
resourceAccess := make(map[string]interface{})
broker := make(map[string]interface{})
broker["roles"] = []string{"read-token"}
resourceAccess["broker"] = broker
account := make(map[string]interface{})
account["roles"] = []string{"manage-account", "manage-account-links", "view-profile"}
resourceAccess["account"] = account
claims["resource_access"] = resourceAccess
return token, nil
}
// GenerateUnsignedUserRefreshToken generates an unsigned OAuth2 user refresh token for the given identity based on the Keycloak token
func (m *tokenManager) GenerateUnsignedUserRefreshTokenForAPIClient(ctx context.Context, accessToken string) (*jwt.Token, error) {
token := jwt.New(jwt.SigningMethodRS256)
token.Header["kid"] = m.userAccountPrivateKey.KeyID
tokenClaims, err := m.ParseToken(ctx, accessToken)
if err != nil {
return nil, errors.WithStack(err)
}
authOpenshiftIO := m.config.GetAuthServiceURL()
openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURLStr(authOpenshiftIO, "", "")
if err != nil {
return nil, errors.WithStack(err)
}
claims := token.Claims.(jwt.MapClaims)
claims["jti"] = uuid.NewV4().String()
iat := time.Now().Unix()
exp := iat + m.config.GetRefreshTokenExpiresIn()
typ := "Refresh"
claims["exp"] = exp
claims["nbf"] = 0
claims["iat"] = iat
claims["iss"] = authOpenshiftIO
claims["aud"] = openshiftIO
claims["typ"] = typ
claims["auth_time"] = 0
// populate claims for user details in refresh token for api_client
claims["sub"] = tokenClaims.Subject
claims["email_verified"] = tokenClaims.EmailVerified
claims["name"] = tokenClaims.Name
claims["preferred_username"] = tokenClaims.Username
claims["given_name"] = tokenClaims.GivenName
claims["family_name"] = tokenClaims.FamilyName
claims["email"] = tokenClaims.Email
// ToDo - Do we need azp claim?
claims["azp"] = tokenClaims.Audience
claims["session_state"] = tokenClaims.SessionState
return token, nil
}
// #####################################################################################################################
//
// General functions
//
// #####################################################################################################################
// JSONWebKeys returns all the public keys in JSON Web Keys format
func (mgm *tokenManager) JSONWebKeys() token.JSONKeys {
return mgm.jsonWebKeys
}
// KeyFunction returns a function that can be used to extract the key ID (kid) claim value from a JWT token
func (m *tokenManager) KeyFunction(ctx context.Context) jwt.Keyfunc {
return func(token *jwt.Token) (interface{}, error) {
kid := token.Header["kid"]
if kid == nil {
log.Error(ctx, map[string]interface{}{}, "There is no 'kid' header in the token")
return nil, errors.New("There is no 'kid' header in the token")
}
key := m.PublicKey(fmt.Sprintf("%s", kid))
if key == nil {
log.Error(ctx, map[string]interface{}{
"kid": kid,
}, "There is no public key with such ID")
return nil, errors.New(fmt.Sprintf("There is no public key with such ID: %s", kid))
}
return key, nil
}
}
// Locate extracts the "sub" claim from the JWT token in the specified token and returns it as a UUID. The
// UUID value typically represents the Identity ID of the current user.
// TODO rename this to something more descriptive
func (m *tokenManager) Locate(ctx context.Context) (uuid.UUID, error) {
token := goajwt.ContextJWT(ctx)
if token == nil {
return uuid.UUID{}, errors.New("Missing token") // TODO, make specific tokenErrors
}
id := token.Claims.(jwt.MapClaims)["sub"]
if id == nil {
return uuid.UUID{}, errors.New("Missing sub")
}
idTyped, err := uuid.FromString(id.(string))
if err != nil {
return uuid.UUID{}, errors.New("uuid not of type string")
}
return idTyped, nil
}
// Parse parses the specified token string and returns a JWT token
func (m *tokenManager) Parse(ctx context.Context, tokenString string) (*jwt.Token, error) {
keyFunc := m.KeyFunction(ctx)
jwtToken, err := jwt.Parse(tokenString, keyFunc)
if err != nil {
log.Error(ctx, map[string]interface{}{
"err": err,
}, "unable to parse token")
return nil, autherrors.NewUnauthorizedError(err.Error())
}
return jwtToken, nil
}
// ParseToken parses the specified token string and returns its claims
func (m *tokenManager) ParseToken(ctx context.Context, tokenString string) (*TokenClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, m.KeyFunction(ctx))
if err != nil {
return nil, err
}
claims := token.Claims.(*TokenClaims)
if token.Valid {
return claims, nil
}
return nil, errors.WithStack(errors.New("token is not valid"))
}
// ParseTokenWithMapClaims parses token claims
func (m *tokenManager) ParseTokenWithMapClaims(ctx context.Context, tokenString string) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, m.KeyFunction(ctx))
if err != nil {
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
if token.Valid {
return claims, nil
}
return nil, errors.WithStack(errors.New("token is not valid"))
}
// PemKeys returns all the public keys in PEM-like format (PEM without header and footer)