diff --git a/.vscode/launch.json b/.vscode/launch.json index a90fc645..b2bb2a6b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -32,7 +32,7 @@ "port": 2345, "host": "127.0.0.1", "program": "${workspaceFolder}/cmd/boltdb/main.go", - "cwd": "${workspaceFolder}/cmd/boltdb/", + "cwd": "${workspaceFolder}", "env": { "SES_REGION": "us-east-1", "SES_SENDER": "noreply@google.com", diff --git a/cmd/boltdb/main.go b/cmd/boltdb/main.go index d41a483d..190d7309 100644 --- a/cmd/boltdb/main.go +++ b/cmd/boltdb/main.go @@ -11,8 +11,8 @@ import ( const ( testAppID = "59fd884d8f6b180001f5b4e2" - appsImportPath = "../import/apps.json" - usersImportPath = "../import/users.json" + appsImportPath = "./cmd/import/apps.json" + usersImportPath = "./cmd/import/users.json" ) func initServer() model.Server { diff --git a/jwt/service/jwt_token_service.go b/jwt/service/jwt_token_service.go index c716650d..27b48101 100644 --- a/jwt/service/jwt_token_service.go +++ b/jwt/service/jwt_token_service.go @@ -159,7 +159,7 @@ func (ts *JWTokenService) ValidateTokenString(tstr string, v jwtValidator.Valida } // NewAccessToken creates new access token for user. -func (ts *JWTokenService) NewAccessToken(u model.User, scopes []string, app model.AppData, requireTFA bool) (ijwt.Token, error) { +func (ts *JWTokenService) NewAccessToken(u model.User, scopes []string, app model.AppData, requireTFA bool, tokenPayload map[string]interface{}) (ijwt.Token, error) { if !app.Active() { return nil, ErrInvalidApp } @@ -168,7 +168,7 @@ func (ts *JWTokenService) NewAccessToken(u model.User, scopes []string, app mode return nil, ErrInvalidUser } - payload := make(map[string]string) + payload := make(map[string]interface{}) if contains(app.TokenPayload(), PayloadName) { payload[PayloadName] = u.Username() } @@ -177,6 +177,11 @@ func (ts *JWTokenService) NewAccessToken(u model.User, scopes []string, app mode if requireTFA { scopes = []string{model.TokenTFAPreauthScope} } + if len(tokenPayload) > 0 { + for k, v := range tokenPayload { + payload[k] = v + } + } now := ijwt.TimeFunc().Unix() @@ -229,7 +234,7 @@ func (ts *JWTokenService) NewRefreshToken(u model.User, scopes []string, app mod return nil, ErrInvalidUser } - payload := make(map[string]string) + payload := make(map[string]interface{}) if contains(app.TokenPayload(), PayloadName) { payload[PayloadName] = u.Username() } @@ -306,7 +311,7 @@ func (ts *JWTokenService) RefreshAccessToken(refreshToken ijwt.Token) (ijwt.Toke return nil, ErrInvalidUser } - token, err := ts.NewAccessToken(user, strings.Split(claims.Scopes, " "), app, false) + token, err := ts.NewAccessToken(user, strings.Split(claims.Scopes, " "), app, false, nil) if err != nil { return nil, err } @@ -324,7 +329,7 @@ func (ts *JWTokenService) RefreshAccessToken(refreshToken ijwt.Token) (ijwt.Toke // NewInviteToken creates new invite token. func (ts *JWTokenService) NewInviteToken(email string) (ijwt.Token, error) { - payload := make(map[string]string) + payload := make(map[string]interface{}) // add payload data here if email != "" { payload["email"] = email diff --git a/jwt/service/token_service.go b/jwt/service/token_service.go index 47525da4..b4c738f1 100644 --- a/jwt/service/token_service.go +++ b/jwt/service/token_service.go @@ -12,7 +12,7 @@ const ( // TokenService is an abstract token manager. type TokenService interface { - NewAccessToken(u model.User, scopes []string, app model.AppData, requireTFA bool) (ijwt.Token, error) + NewAccessToken(u model.User, scopes []string, app model.AppData, requireTFA bool, tokenPayload map[string]interface{}) (ijwt.Token, error) NewRefreshToken(u model.User, scopes []string, app model.AppData) (ijwt.Token, error) RefreshAccessToken(token ijwt.Token) (ijwt.Token, error) NewInviteToken(email string) (ijwt.Token, error) diff --git a/jwt/token.go b/jwt/token.go index 13eaf56b..3ea5e40d 100644 --- a/jwt/token.go +++ b/jwt/token.go @@ -25,7 +25,7 @@ type Token interface { UserID() string Type() string Scopes() string - Payload() map[string]string + Payload() map[string]interface{} } // NewTokenWithClaims generates new JWT token with claims and keyID. @@ -68,10 +68,10 @@ func (t *JWToken) UserID() string { } // Payload returns token payload. -func (t *JWToken) Payload() map[string]string { +func (t *JWToken) Payload() map[string]interface{} { claims, ok := t.JWT.Claims.(*Claims) if !ok { - return make(map[string]string) + return make(map[string]interface{}) } return claims.Payload } @@ -159,10 +159,10 @@ func (t *JWToken) Scopes() string { // Claims is an extended claims structure. type Claims struct { - Payload map[string]string `json:"payload,omitempty"` - Scopes string `json:"scopes,omitempty"` - Type string `json:"type,omitempty"` - KeyID string `json:"kid,omitempty"` // optional keyID + Payload map[string]interface{} `json:"payload,omitempty"` + Scopes string `json:"scopes,omitempty"` + Type string `json:"type,omitempty"` + KeyID string `json:"kid,omitempty"` // optional keyID jwt.StandardClaims } diff --git a/jwt/token_test.go b/jwt/token_test.go index 40289529..c0c8a2da 100644 --- a/jwt/token_test.go +++ b/jwt/token_test.go @@ -217,7 +217,6 @@ func TestTokenToString(t *testing.T) { if !reflect.DeepEqual(claims1, claims2) { t.Errorf("Claims = %+v, want %+v", claims1, claims2) } - } func TestNewToken(t *testing.T) { @@ -253,14 +252,14 @@ func TestNewToken(t *testing.T) { } ustg, _ := mem.NewUserStorage() user, _ := ustg.UserByNamePassword("username", "password") - //generate random user until we get active user + // generate random user until we get active user for !user.Active() { user, _ = ustg.UserByNamePassword("username", "password") } scopes := []string{"scope1", "scope2"} tokenPayload := []string{"name"} app := mem.MakeAppData("123456", "1", true, "testName", "testDescriprion", scopes, true, []string{}, 0, 0, 0, tokenPayload, true, true, model.TFAStatusDisabled, "", model.NoAuthz, "", "", []string{}, []string{}, "user") - token, err := ts.NewAccessToken(user, scopes, &app, false) + token, err := ts.NewAccessToken(user, scopes, &app, false, nil) if err != nil { t.Errorf("Unable to create token %v", err) } diff --git a/model/app_storage.go b/model/app_storage.go index 41e50b73..3beb8484 100644 --- a/model/app_storage.go +++ b/model/app_storage.go @@ -52,6 +52,11 @@ type AppData interface { NewUserDefaultRole() string AppleInfo() *AppleInfo SetSecret(secret string) + + //Token payload related services + TokenPayloadService() TokenPayloadServiceType + TokenPayloadServicePluginSettings() TokenPayloadServicePluginSettings + TokenPayloadServiceHttpSettings() TokenPayloadServiceHttpSettings } // AppType is a type of application. @@ -95,3 +100,26 @@ const ( // TFAStatusDisabled is when the app does not support TFA. TFAStatusDisabled = "disabled" ) + +// TokenPayloadServiceType service to allow fetch additional data to include to access token +type TokenPayloadServiceType string + +const ( + // TokenPayloadServiceNone no service is used + TokenPayloadServiceNone = "none" + // TokenPayloadServicePlugin user local identifo plugin with specific name to retreive token payload + TokenPayloadServicePlugin = "plugin" + // TokenPayloadServiceHttp use external service to get token paylad + TokenPayloadServiceHttp = "http" +) + +// TokenPayloadServicePluginSettings settings for token payload service +type TokenPayloadServicePluginSettings struct { + Name string `json:"name,omitempty" bson:"name,omitempty"` +} + +// TokenPayloadServiceHttpSettings settings for token payload service +type TokenPayloadServiceHttpSettings struct { + URL string `json:"url,omitempty" bson:"url,omitempty"` + Secret string `json:"secret,omitempty" bson:"secret,omitempty"` +} diff --git a/model/user_data_provider.go b/model/user_data_provider.go new file mode 100644 index 00000000..c97a6d79 --- /dev/null +++ b/model/user_data_provider.go @@ -0,0 +1,6 @@ +package model + +//TokenPayloadProvider provides additional user payload to include to the token +type TokenPayloadProvider interface { + TokenPayloadForApp(appId, appName, userId string) (map[string]interface{}, error) +} diff --git a/storage/boltdb/app.go b/storage/boltdb/app.go index 27abb599..e01a4124 100644 --- a/storage/boltdb/app.go +++ b/storage/boltdb/app.go @@ -12,49 +12,55 @@ type AppData struct { } type appData struct { - ID string `json:"id,omitempty"` - Secret string `json:"secret,omitempty"` - Active bool `json:"active"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Scopes []string `json:"scopes,omitempty"` - Offline bool `json:"offline"` - Type model.AppType `json:"type,omitempty"` - RedirectURLs []string `json:"redirect_urls,omitempty"` - RefreshTokenLifespan int64 `json:"refresh_token_lifespan,omitempty"` - InviteTokenLifespan int64 `json:"invite_token_lifespan,omitempty"` - TokenLifespan int64 `json:"token_lifespan,omitempty"` - TokenPayload []string `json:"token_payload,omitempty"` - RegistrationForbidden bool `json:"registration_forbidden"` - AnonymousRegistrationAllowed bool `json:"anonymous_registration_allowed"` - TFAStatus model.TFAStatus `json:"tfa_status"` - DebugTFACode string `json:"debug_tfa_code,omitempty"` - AuthorizationWay model.AuthorizationWay `json:"authorization_way,omitempty"` - AuthorizationModel string `json:"authorization_model,omitempty"` - AuthorizationPolicy string `json:"authorization_policy,omitempty"` - RolesWhitelist []string `json:"roles_whitelist,omitempty"` - RolesBlacklist []string `json:"roles_blacklist,omitempty"` - NewUserDefaultRole string `json:"new_user_default_role,omitempty"` - AppleInfo *model.AppleInfo `json:"apple_info,omitempty"` + ID string `json:"id,omitempty"` + Secret string `json:"secret,omitempty"` + Active bool `json:"active,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Scopes []string `json:"scopes,omitempty"` + Offline bool `json:"offline,omitempty"` + Type model.AppType `json:"type,omitempty"` + RedirectURLs []string `json:"redirect_urls,omitempty"` + RefreshTokenLifespan int64 `json:"refresh_token_lifespan,omitempty"` + InviteTokenLifespan int64 `json:"invite_token_lifespan,omitempty"` + TokenLifespan int64 `json:"token_lifespan,omitempty"` + TokenPayload []string `json:"token_payload,omitempty"` + RegistrationForbidden bool `json:"registration_forbidden,omitempty"` + AnonymousRegistrationAllowed bool `json:"anonymous_registration_allowed,omitempty"` + TFAStatus model.TFAStatus `json:"tfa_status"` + DebugTFACode string `json:"debug_tfa_code,omitempty"` + AuthorizationWay model.AuthorizationWay `json:"authorization_way,omitempty"` + AuthorizationModel string `json:"authorization_model,omitempty"` + AuthorizationPolicy string `json:"authorization_policy,omitempty"` + RolesWhitelist []string `json:"roles_whitelist"` + RolesBlacklist []string `json:"roles_blacklist,omitempty"` + NewUserDefaultRole string `json:"new_user_default_role,omitempty"` + AppleInfo *model.AppleInfo `json:"apple_info,omitempty"` + TokenPayloadService model.TokenPayloadServiceType `json:"token_payload_service"` + TokenPayloadServicePluginSettings model.TokenPayloadServicePluginSettings `json:"token_payload_service_plugin_settings,omitempty"` + TokenPayloadServiceHttpSettings model.TokenPayloadServiceHttpSettings `json:"token_payload_service_http_settings,omitempty"` } // NewAppData instantiates in-memory app data model from the general one. func NewAppData(data model.AppData) AppData { return AppData{appData: appData{ - ID: data.ID(), - Secret: data.Secret(), - Active: data.Active(), - Name: data.Name(), - Description: data.Description(), - Scopes: data.Scopes(), - Offline: data.Offline(), - RedirectURLs: data.RedirectURLs(), - RefreshTokenLifespan: data.RefreshTokenLifespan(), - InviteTokenLifespan: data.InviteTokenLifespan(), - TokenLifespan: data.TokenLifespan(), - TokenPayload: data.TokenPayload(), - RegistrationForbidden: data.RegistrationForbidden(), - AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), + ID: data.ID(), + Secret: data.Secret(), + Active: data.Active(), + Name: data.Name(), + Description: data.Description(), + Scopes: data.Scopes(), + Offline: data.Offline(), + RedirectURLs: data.RedirectURLs(), + RefreshTokenLifespan: data.RefreshTokenLifespan(), + InviteTokenLifespan: data.InviteTokenLifespan(), + TokenLifespan: data.TokenLifespan(), + TokenPayload: data.TokenPayload(), + RegistrationForbidden: data.RegistrationForbidden(), + AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), + TokenPayloadService: data.TokenPayloadService(), + TokenPayloadServicePluginSettings: data.TokenPayloadServicePluginSettings(), + TokenPayloadServiceHttpSettings: data.TokenPayloadServiceHttpSettings(), }} } @@ -94,6 +100,7 @@ func MakeAppData(id, secret string, active bool, name, description string, scope RolesWhitelist: rolesWhitelist, RolesBlacklist: rolesBlacklist, NewUserDefaultRole: newUserDefaultRole, + TokenPayloadService: model.TokenPayloadServiceNone, }} } @@ -145,7 +152,9 @@ func (ad *AppData) TokenPayload() []string { return ad.appData.TokenPayload } func (ad *AppData) RegistrationForbidden() bool { return ad.appData.RegistrationForbidden } // AnonymousRegistrationAllowed implements model.AppData interface. -func (ad *AppData) AnonymousRegistrationAllowed() bool { return ad.appData.AnonymousRegistrationAllowed } +func (ad *AppData) AnonymousRegistrationAllowed() bool { + return ad.appData.AnonymousRegistrationAllowed +} // TFAStatus implements model.AppData interface. func (ad *AppData) TFAStatus() model.TFAStatus { return ad.appData.TFAStatus } @@ -174,6 +183,18 @@ func (ad *AppData) NewUserDefaultRole() string { return ad.appData.NewUserDefaul // AppleInfo implements model.AppData interface. func (ad *AppData) AppleInfo() *model.AppleInfo { return ad.appData.AppleInfo } +func (ad *AppData) TokenPayloadService() model.TokenPayloadServiceType { + return ad.appData.TokenPayloadService +} + +func (ad *AppData) TokenPayloadServicePluginSettings() model.TokenPayloadServicePluginSettings { + return ad.appData.TokenPayloadServicePluginSettings +} + +func (ad *AppData) TokenPayloadServiceHttpSettings() model.TokenPayloadServiceHttpSettings { + return ad.appData.TokenPayloadServiceHttpSettings +} + // SetSecret implements model.AppData interface. func (ad *AppData) SetSecret(secret string) { if ad == nil { @@ -195,4 +216,6 @@ func (ad *AppData) Sanitize() { ad.appData.AuthorizationWay = "" ad.appData.AuthorizationModel = "" ad.appData.AuthorizationPolicy = "" + ad.appData.TokenPayloadServiceHttpSettings = model.TokenPayloadServiceHttpSettings{} + ad.appData.TokenPayloadServicePluginSettings = model.TokenPayloadServicePluginSettings{} } diff --git a/storage/dynamodb/app.go b/storage/dynamodb/app.go index 385dac63..e78594e7 100644 --- a/storage/dynamodb/app.go +++ b/storage/dynamodb/app.go @@ -14,30 +14,33 @@ type AppData struct { } type appData struct { - ID string `json:"id,omitempty"` - Secret string `json:"secret,omitempty"` - Active bool `json:"active"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Scopes []string `json:"scopes,omitempty"` - Offline bool `json:"offline"` - Type model.AppType `json:"type,omitempty"` - RedirectURLs []string `json:"redirect_urls,omitempty"` - RefreshTokenLifespan int64 `json:"refresh_token_lifespan,omitempty"` - InviteTokenLifespan int64 `json:"invite_token_lifespan,omitempty"` - TokenLifespan int64 `json:"token_lifespan,omitempty"` - TokenPayload []string `json:"token_payload,omitempty"` - TFAStatus model.TFAStatus `json:"tfa_status"` - DebugTFACode string `json:"debug_tfa_code,omitempty"` - RegistrationForbidden bool `json:"registration_forbidden"` - AnonymousRegistrationAllowed bool `json:"anonymous_registration_allowed"` - AuthorizationWay model.AuthorizationWay `json:"authorization_way,omitempty"` - AuthorizationModel string `json:"authorization_model,omitempty"` - AuthorizationPolicy string `json:"authorization_policy,omitempty"` - RolesWhitelist []string `json:"roles_whitelist,omitempty"` - RolesBlacklist []string `json:"roles_blacklist,omitempty"` - NewUserDefaultRole string `json:"new_user_default_role,omitempty"` - AppleInfo *model.AppleInfo `json:"apple_info,omitempty"` + ID string `json:"id,omitempty"` + Secret string `json:"secret,omitempty"` + Active bool `json:"active"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Scopes []string `json:"scopes,omitempty"` + Offline bool `json:"offline"` + Type model.AppType `json:"type,omitempty"` + RedirectURLs []string `json:"redirect_urls,omitempty"` + RefreshTokenLifespan int64 `json:"refresh_token_lifespan,omitempty"` + InviteTokenLifespan int64 `json:"invite_token_lifespan,omitempty"` + TokenLifespan int64 `json:"token_lifespan,omitempty"` + TokenPayload []string `json:"token_payload,omitempty"` + TFAStatus model.TFAStatus `json:"tfa_status"` + DebugTFACode string `json:"debug_tfa_code,omitempty"` + RegistrationForbidden bool `json:"registration_forbidden"` + AnonymousRegistrationAllowed bool `json:"anonymous_registration_allowed"` + AuthorizationWay model.AuthorizationWay `json:"authorization_way,omitempty"` + AuthorizationModel string `json:"authorization_model,omitempty"` + AuthorizationPolicy string `json:"authorization_policy,omitempty"` + RolesWhitelist []string `json:"roles_whitelist,omitempty"` + RolesBlacklist []string `json:"roles_blacklist,omitempty"` + NewUserDefaultRole string `json:"new_user_default_role,omitempty"` + AppleInfo *model.AppleInfo `json:"apple_info,omitempty"` + TokenPayloadService model.TokenPayloadServiceType `json:"token_payload_service"` + TokenPayloadServicePluginSettings model.TokenPayloadServicePluginSettings `json:"token_payload_service_plugin_settings,omitempty"` + TokenPayloadServiceHttpSettings model.TokenPayloadServiceHttpSettings `json:"token_payload_service_http_settings,omitempty"` } // NewAppData instantiates DynamoDB app data model from the general one. @@ -47,20 +50,23 @@ func NewAppData(data model.AppData) (AppData, error) { return AppData{}, model.ErrorWrongDataFormat } return AppData{appData: appData{ - ID: data.ID(), - Secret: data.Secret(), - Active: data.Active(), - Name: data.Name(), - Description: data.Description(), - Scopes: data.Scopes(), - Offline: data.Offline(), - RedirectURLs: data.RedirectURLs(), - RefreshTokenLifespan: data.RefreshTokenLifespan(), - InviteTokenLifespan: data.InviteTokenLifespan(), - TokenLifespan: data.TokenLifespan(), - TokenPayload: data.TokenPayload(), - RegistrationForbidden: data.RegistrationForbidden(), - AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), + ID: data.ID(), + Secret: data.Secret(), + Active: data.Active(), + Name: data.Name(), + Description: data.Description(), + Scopes: data.Scopes(), + Offline: data.Offline(), + RedirectURLs: data.RedirectURLs(), + RefreshTokenLifespan: data.RefreshTokenLifespan(), + InviteTokenLifespan: data.InviteTokenLifespan(), + TokenLifespan: data.TokenLifespan(), + TokenPayload: data.TokenPayload(), + RegistrationForbidden: data.RegistrationForbidden(), + AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), + TokenPayloadService: data.TokenPayloadService(), + TokenPayloadServicePluginSettings: data.TokenPayloadServicePluginSettings(), + TokenPayloadServiceHttpSettings: data.TokenPayloadServiceHttpSettings(), }}, nil } @@ -105,6 +111,7 @@ func MakeAppData(id, secret string, active bool, name, description string, scope RolesWhitelist: rolesWhitelist, RolesBlacklist: rolesBlacklist, NewUserDefaultRole: newUserDefaultRole, + TokenPayloadService: model.TokenPayloadServiceNone, }}, nil } @@ -162,7 +169,9 @@ func (ad *AppData) DebugTFACode() string { return ad.appData.DebugTFACode } func (ad *AppData) RegistrationForbidden() bool { return ad.appData.RegistrationForbidden } // AnonymousRegistrationAllowed implements model.AppData interface. -func (ad *AppData) AnonymousRegistrationAllowed() bool { return ad.appData.AnonymousRegistrationAllowed } +func (ad *AppData) AnonymousRegistrationAllowed() bool { + return ad.appData.AnonymousRegistrationAllowed +} // AuthzWay implements model.AppData interface. func (ad *AppData) AuthzWay() model.AuthorizationWay { return ad.appData.AuthorizationWay } @@ -185,6 +194,18 @@ func (ad *AppData) NewUserDefaultRole() string { return ad.appData.NewUserDefaul // AppleInfo implements model.AppData interface. func (ad *AppData) AppleInfo() *model.AppleInfo { return ad.appData.AppleInfo } +func (ad *AppData) TokenPayloadService() model.TokenPayloadServiceType { + return ad.appData.TokenPayloadService +} + +func (ad *AppData) TokenPayloadServicePluginSettings() model.TokenPayloadServicePluginSettings { + return ad.appData.TokenPayloadServicePluginSettings +} + +func (ad *AppData) TokenPayloadServiceHttpSettings() model.TokenPayloadServiceHttpSettings { + return ad.appData.TokenPayloadServiceHttpSettings +} + // SetSecret implements model.AppData interface. func (ad *AppData) SetSecret(secret string) { if ad == nil { @@ -206,4 +227,6 @@ func (ad *AppData) Sanitize() { ad.appData.AuthorizationWay = "" ad.appData.AuthorizationModel = "" ad.appData.AuthorizationPolicy = "" + ad.appData.TokenPayloadServiceHttpSettings = model.TokenPayloadServiceHttpSettings{} + ad.appData.TokenPayloadServicePluginSettings = model.TokenPayloadServicePluginSettings{} } diff --git a/storage/mem/app.go b/storage/mem/app.go index cb4cd249..3fa432b3 100644 --- a/storage/mem/app.go +++ b/storage/mem/app.go @@ -10,49 +10,55 @@ type AppData struct { } type appData struct { - ID string `json:"id,omitempty"` - Secret string `json:"secret,omitempty"` - Active bool `json:"active"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Scopes []string `json:"scopes,omitempty"` - Offline bool `json:"offline"` - Type model.AppType `json:"type,omitempty"` - RedirectURLs []string `json:"redirect_urls,omitempty"` - RefreshTokenLifespan int64 `json:"refresh_token_lifespan,omitempty"` - InviteTokenLifespan int64 `json:"invite_token_lifespan,omitempty"` - TokenLifespan int64 `json:"token_lifespan,omitempty"` - TokenPayload []string `json:"token_payload,omitempty"` - RegistrationForbidden bool `json:"registration_forbidden"` - AnonymousRegistrationAllowed bool `json:"anonymous_registration_allowed"` - TFAStatus model.TFAStatus `json:"tfa_status"` - DebugTFACode string `json:"debug_tfa_code,omitempty"` - AuthorizationWay model.AuthorizationWay `json:"authorization_way,omitempty"` - AuthorizationModel string `json:"authorization_model,omitempty"` - AuthorizationPolicy string `json:"authorization_policy,omitempty"` - RolesWhitelist []string `json:"roles_whitelist,omitempty"` - RolesBlacklist []string `json:"roles_blacklist,omitempty"` - NewUserDefaultRole string `json:"new_user_default_role,omitempty"` - AppleInfo *model.AppleInfo `json:"apple_info,omitempty"` + ID string `json:"id,omitempty"` + Secret string `json:"secret,omitempty"` + Active bool `json:"active"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Scopes []string `json:"scopes,omitempty"` + Offline bool `json:"offline"` + Type model.AppType `json:"type,omitempty"` + RedirectURLs []string `json:"redirect_urls,omitempty"` + RefreshTokenLifespan int64 `json:"refresh_token_lifespan,omitempty"` + InviteTokenLifespan int64 `json:"invite_token_lifespan,omitempty"` + TokenLifespan int64 `json:"token_lifespan,omitempty"` + TokenPayload []string `json:"token_payload,omitempty"` + RegistrationForbidden bool `json:"registration_forbidden"` + AnonymousRegistrationAllowed bool `json:"anonymous_registration_allowed"` + TFAStatus model.TFAStatus `json:"tfa_status"` + DebugTFACode string `json:"debug_tfa_code,omitempty"` + AuthorizationWay model.AuthorizationWay `json:"authorization_way,omitempty"` + AuthorizationModel string `json:"authorization_model,omitempty"` + AuthorizationPolicy string `json:"authorization_policy,omitempty"` + RolesWhitelist []string `json:"roles_whitelist,omitempty"` + RolesBlacklist []string `json:"roles_blacklist,omitempty"` + NewUserDefaultRole string `json:"new_user_default_role,omitempty"` + AppleInfo *model.AppleInfo `json:"apple_info,omitempty"` + TokenPayloadService model.TokenPayloadServiceType `json:"token_payload_service"` + TokenPayloadServicePluginSettings model.TokenPayloadServicePluginSettings `json:"token_payload_service_plugin_settings,omitempty"` + TokenPayloadServiceHttpSettings model.TokenPayloadServiceHttpSettings `json:"token_payload_service_http_settings,omitempty"` } // NewAppData instantiates app data in-memory model from the general one. func NewAppData(data model.AppData) AppData { return AppData{appData: appData{ - ID: data.ID(), - Secret: data.Secret(), - Active: data.Active(), - Name: data.Name(), - Description: data.Description(), - Scopes: data.Scopes(), - Offline: data.Offline(), - RedirectURLs: data.RedirectURLs(), - RefreshTokenLifespan: data.RefreshTokenLifespan(), - InviteTokenLifespan: data.InviteTokenLifespan(), - TokenLifespan: data.TokenLifespan(), - TokenPayload: data.TokenPayload(), - RegistrationForbidden: data.RegistrationForbidden(), - AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), + ID: data.ID(), + Secret: data.Secret(), + Active: data.Active(), + Name: data.Name(), + Description: data.Description(), + Scopes: data.Scopes(), + Offline: data.Offline(), + RedirectURLs: data.RedirectURLs(), + RefreshTokenLifespan: data.RefreshTokenLifespan(), + InviteTokenLifespan: data.InviteTokenLifespan(), + TokenLifespan: data.TokenLifespan(), + TokenPayload: data.TokenPayload(), + RegistrationForbidden: data.RegistrationForbidden(), + AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), + TokenPayloadService: data.TokenPayloadService(), + TokenPayloadServicePluginSettings: data.TokenPayloadServicePluginSettings(), + TokenPayloadServiceHttpSettings: data.TokenPayloadServiceHttpSettings(), }} } @@ -84,6 +90,7 @@ func MakeAppData(id, secret string, active bool, name, description string, scope RolesWhitelist: rolesWhitelist, RolesBlacklist: rolesBlacklist, NewUserDefaultRole: newUserDefaultRole, + TokenPayloadService: model.TokenPayloadServiceNone, }} } @@ -130,7 +137,9 @@ func (ad *AppData) TokenPayload() []string { return ad.appData.TokenPayload } func (ad *AppData) RegistrationForbidden() bool { return ad.appData.RegistrationForbidden } // AnonymousRegistrationAllowed implements model.AppData interface. -func (ad *AppData) AnonymousRegistrationAllowed() bool { return ad.appData.AnonymousRegistrationAllowed } +func (ad *AppData) AnonymousRegistrationAllowed() bool { + return ad.appData.AnonymousRegistrationAllowed +} // TFAStatus implements model.AppData interface. func (ad *AppData) TFAStatus() model.TFAStatus { return ad.appData.TFAStatus } @@ -159,6 +168,18 @@ func (ad *AppData) NewUserDefaultRole() string { return ad.appData.NewUserDefaul // AppleInfo implements model.AppData interface. func (ad *AppData) AppleInfo() *model.AppleInfo { return ad.appData.AppleInfo } +func (ad *AppData) TokenPayloadService() model.TokenPayloadServiceType { + return ad.appData.TokenPayloadService +} + +func (ad *AppData) TokenPayloadServicePluginSettings() model.TokenPayloadServicePluginSettings { + return ad.appData.TokenPayloadServicePluginSettings +} + +func (ad *AppData) TokenPayloadServiceHttpSettings() model.TokenPayloadServiceHttpSettings { + return ad.appData.TokenPayloadServiceHttpSettings +} + // SetSecret implements model.AppData interface. func (ad *AppData) SetSecret(secret string) { if ad == nil { @@ -180,4 +201,7 @@ func (ad *AppData) Sanitize() { ad.appData.AuthorizationWay = "" ad.appData.AuthorizationModel = "" ad.appData.AuthorizationPolicy = "" + ad.appData.TokenPayloadServiceHttpSettings = model.TokenPayloadServiceHttpSettings{} + ad.appData.TokenPayloadServicePluginSettings = model.TokenPayloadServicePluginSettings{} + } diff --git a/storage/mongo/app.go b/storage/mongo/app.go index 2191218f..9c0b9fb5 100644 --- a/storage/mongo/app.go +++ b/storage/mongo/app.go @@ -13,30 +13,33 @@ type AppData struct { } type appData struct { - ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` // TODO: use string? - Secret string `bson:"secret,omitempty" json:"secret,omitempty"` - Active bool `bson:"active" json:"active"` - Name string `bson:"name,omitempty" json:"name,omitempty"` - Description string `bson:"description,omitempty" json:"description,omitempty"` - Scopes []string `bson:"scopes,omitempty" json:"scopes,omitempty"` - Offline bool `bson:"offline" json:"offline"` - Type model.AppType `bson:"type,omitempty" json:"type,omitempty"` - RedirectURLs []string `bson:"redirect_urls,omitempty" json:"redirect_urls,omitempty"` - RefreshTokenLifespan int64 `bson:"refresh_token_lifespan,omitempty" json:"refresh_token_lifespan,omitempty"` - InviteTokenLifespan int64 `bson:"invite_token_lifespan,omitempty" json:"invite_token_lifespan,omitempty"` - TokenLifespan int64 `bson:"token_lifespan,omitempty" json:"token_lifespan,omitempty"` - TokenPayload []string `bson:"token_payload,omitempty" json:"token_payload,omitempty"` - RegistrationForbidden bool `bson:"registration_forbidden" json:"registration_forbidden"` - AnonymousRegistrationAllowed bool `bson:"anonymous_registration_allowed" json:"anonymous_registration_allowed"` - TFAStatus model.TFAStatus `bson:"tfa_status" json:"tfa_status"` - DebugTFACode string `bson:"debug_tfa_code,omitempty" json:"debug_tfa_code,omitempty"` - AuthorizationWay model.AuthorizationWay `bson:"authorization_way,omitempty" json:"authorization_way,omitempty"` - AuthorizationModel string `bson:"authorization_model,omitempty" json:"authorization_model,omitempty"` - AuthorizationPolicy string `bson:"authorization_policy,omitempty" json:"authorization_policy,omitempty"` - RolesWhitelist []string `bson:"roles_whitelist,omitempty" json:"roles_whitelist,omitempty"` - RolesBlacklist []string `bson:"roles_blacklist,omitempty" json:"roles_blacklist,omitempty"` - NewUserDefaultRole string `bson:"new_user_default_role,omitempty" json:"new_user_default_role,omitempty"` - AppleInfo *model.AppleInfo `bson:"apple_info,omitempty" json:"apple_info,omitempty"` + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` // TODO: use string? + Secret string `bson:"secret,omitempty" json:"secret,omitempty"` + Active bool `bson:"active" json:"active"` + Name string `bson:"name,omitempty" json:"name,omitempty"` + Description string `bson:"description,omitempty" json:"description,omitempty"` + Scopes []string `bson:"scopes,omitempty" json:"scopes,omitempty"` + Offline bool `bson:"offline" json:"offline"` + Type model.AppType `bson:"type,omitempty" json:"type,omitempty"` + RedirectURLs []string `bson:"redirect_urls,omitempty" json:"redirect_urls,omitempty"` + RefreshTokenLifespan int64 `bson:"refresh_token_lifespan,omitempty" json:"refresh_token_lifespan,omitempty"` + InviteTokenLifespan int64 `bson:"invite_token_lifespan,omitempty" json:"invite_token_lifespan,omitempty"` + TokenLifespan int64 `bson:"token_lifespan,omitempty" json:"token_lifespan,omitempty"` + TokenPayload []string `bson:"token_payload,omitempty" json:"token_payload,omitempty"` + RegistrationForbidden bool `bson:"registration_forbidden" json:"registration_forbidden"` + AnonymousRegistrationAllowed bool `bson:"anonymous_registration_allowed" json:"anonymous_registration_allowed"` + TFAStatus model.TFAStatus `bson:"tfa_status" json:"tfa_status"` + DebugTFACode string `bson:"debug_tfa_code,omitempty" json:"debug_tfa_code,omitempty"` + AuthorizationWay model.AuthorizationWay `bson:"authorization_way,omitempty" json:"authorization_way,omitempty"` + AuthorizationModel string `bson:"authorization_model,omitempty" json:"authorization_model,omitempty"` + AuthorizationPolicy string `bson:"authorization_policy,omitempty" json:"authorization_policy,omitempty"` + RolesWhitelist []string `bson:"roles_whitelist" json:"roles_whitelist"` + RolesBlacklist []string `bson:"roles_blacklist,omitempty" json:"roles_blacklist,omitempty"` + NewUserDefaultRole string `bson:"new_user_default_role,omitempty" json:"new_user_default_role,omitempty"` + AppleInfo *model.AppleInfo `bson:"apple_info,omitempty" json:"apple_info,omitempty"` + TokenPayloadService model.TokenPayloadServiceType `bson:"token_payload_service" json:"token_payload_service"` + TokenPayloadServicePluginSettings model.TokenPayloadServicePluginSettings `bson:"token_payload_service_plugin_settings,omitempty" json:"token_payload_service_plugin_settings,omitempty"` + TokenPayloadServiceHttpSettings model.TokenPayloadServiceHttpSettings `bson:"token_payload_service_http_settings,omitempty" json:"token_payload_service_http_settings,omitempty"` } // NewAppData instantiates MongoDB app data model from the general one. @@ -46,27 +49,30 @@ func NewAppData(data model.AppData) (AppData, error) { return AppData{}, err } return AppData{appData: appData{ - ID: hexID, - Secret: data.Secret(), - Active: data.Active(), - Name: data.Name(), - Description: data.Description(), - Scopes: data.Scopes(), - Offline: data.Offline(), - RedirectURLs: data.RedirectURLs(), - RefreshTokenLifespan: data.RefreshTokenLifespan(), - InviteTokenLifespan: data.InviteTokenLifespan(), - TokenLifespan: data.TokenLifespan(), - TokenPayload: data.TokenPayload(), - RegistrationForbidden: data.RegistrationForbidden(), - AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), - TFAStatus: data.TFAStatus(), - AuthorizationWay: data.AuthzWay(), - AuthorizationModel: data.AuthzModel(), - AuthorizationPolicy: data.AuthzPolicy(), - RolesWhitelist: data.RolesWhitelist(), - RolesBlacklist: data.RolesBlacklist(), - NewUserDefaultRole: data.NewUserDefaultRole(), + ID: hexID, + Secret: data.Secret(), + Active: data.Active(), + Name: data.Name(), + Description: data.Description(), + Scopes: data.Scopes(), + Offline: data.Offline(), + RedirectURLs: data.RedirectURLs(), + RefreshTokenLifespan: data.RefreshTokenLifespan(), + InviteTokenLifespan: data.InviteTokenLifespan(), + TokenLifespan: data.TokenLifespan(), + TokenPayload: data.TokenPayload(), + RegistrationForbidden: data.RegistrationForbidden(), + AnonymousRegistrationAllowed: data.AnonymousRegistrationAllowed(), + TFAStatus: data.TFAStatus(), + AuthorizationWay: data.AuthzWay(), + AuthorizationModel: data.AuthzModel(), + AuthorizationPolicy: data.AuthzPolicy(), + RolesWhitelist: data.RolesWhitelist(), + RolesBlacklist: data.RolesBlacklist(), + NewUserDefaultRole: data.NewUserDefaultRole(), + TokenPayloadService: data.TokenPayloadService(), + TokenPayloadServicePluginSettings: data.TokenPayloadServicePluginSettings(), + TokenPayloadServiceHttpSettings: data.TokenPayloadServiceHttpSettings(), }}, nil } @@ -111,6 +117,7 @@ func MakeAppData(id, secret string, active bool, name, description string, scope RolesWhitelist: rolesWhitelist, RolesBlacklist: rolesBlacklist, NewUserDefaultRole: newUserDefaultRole, + TokenPayloadService: model.TokenPayloadServiceNone, }}, nil } @@ -162,7 +169,9 @@ func (ad *AppData) TokenPayload() []string { return ad.appData.TokenPayload } func (ad *AppData) RegistrationForbidden() bool { return ad.appData.RegistrationForbidden } // AnonymousRegistrationAllowed implements model.AppData interface. -func (ad *AppData) AnonymousRegistrationAllowed() bool { return ad.appData.AnonymousRegistrationAllowed } +func (ad *AppData) AnonymousRegistrationAllowed() bool { + return ad.appData.AnonymousRegistrationAllowed +} // TFAStatus implements model.AppData interface. func (ad *AppData) TFAStatus() model.TFAStatus { return ad.appData.TFAStatus } @@ -191,6 +200,18 @@ func (ad *AppData) NewUserDefaultRole() string { return ad.appData.NewUserDefaul // AppleInfo implements model.AppData interface. func (ad *AppData) AppleInfo() *model.AppleInfo { return ad.appData.AppleInfo } +func (ad *AppData) TokenPayloadService() model.TokenPayloadServiceType { + return ad.appData.TokenPayloadService +} + +func (ad *AppData) TokenPayloadServicePluginSettings() model.TokenPayloadServicePluginSettings { + return ad.appData.TokenPayloadServicePluginSettings +} + +func (ad *AppData) TokenPayloadServiceHttpSettings() model.TokenPayloadServiceHttpSettings { + return ad.appData.TokenPayloadServiceHttpSettings +} + // SetSecret implements model.AppData interface. func (ad *AppData) SetSecret(secret string) { if ad == nil { @@ -212,4 +233,7 @@ func (ad *AppData) Sanitize() { ad.appData.AuthorizationWay = "" ad.appData.AuthorizationModel = "" ad.appData.AuthorizationPolicy = "" + ad.appData.TokenPayloadServiceHttpSettings = model.TokenPayloadServiceHttpSettings{} + ad.appData.TokenPayloadServicePluginSettings = model.TokenPayloadServicePluginSettings{} + } diff --git a/user_payload_provider/http/provider.go b/user_payload_provider/http/provider.go new file mode 100644 index 00000000..2e2f0c8e --- /dev/null +++ b/user_payload_provider/http/provider.go @@ -0,0 +1,83 @@ +package http + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/madappgang/identifo/model" +) + +//NewTokenPayloadProvider creates new HTTP webhood provider +//it basically to the call to 3rd party http service +//to secure this interaction, the receiver should apply some actions to ensure +//the authorized Identity service is doing the request +//To provide that level of verification we are signing the request with HMAC-SHA256 signature +//https://en.wikipedia.org/wiki/HMAC +//We are not using SHA1 here, because SHA2 is more secure. +//We have limited SHA2 with SHA256 simplify the implementation, and SHA256 is the most popular among SHA2 family +//SHA3 is not so popular yet and is limited in client packages available +//Please verify signature on your side +// +//you can also whitelist identifo's IP as an extra step +func NewTokenPayloadProvider(secret string, serviceURL string) (model.TokenPayloadProvider, error) { + if len(secret) < 5 { + return nil, errors.New("http user payload provider init error, the secret is empty or short, it should be at least 5 chars long") + } + _, err := url.Parse(serviceURL) + if err != nil { + return nil, fmt.Errorf("http user payload provider init error, bad service URL , %v", err) + } + p := provider{ + secret: secret, + url: serviceURL, + } + return &p, nil +} + +type provider struct { + secret string + url string +} + +func (p *provider) TokenPayloadForApp(appId, appName, userId string) (map[string]interface{}, error) { + body, _ := json.Marshal(map[string]string{ + "app_id": appId, + "app_name": appName, + "user_id": userId, + }) + h := hmac.New(sha256.New, []byte(p.secret)) + h.Write(body) + sha := hex.EncodeToString(h.Sum(nil)) + + client := &http.Client{} + request, err := http.NewRequest("POST", p.url, bytes.NewBuffer(body)) + if err != nil { + return nil, fmt.Errorf("creating http client fro http user payload provider: %v", err) + } + request.Header.Set("Digest", "SHA-256="+sha) + request.Header.Set("Content-type", "application/json") + resp, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("getting user payload: %v", err) + } + if resp.StatusCode > 299 { + return nil, fmt.Errorf("getting user payload from provider, response code expected 200, got: %d", resp.StatusCode) + } + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("getting user payload from provider, could not read response body with error: %v", err) + } + var result map[string]interface{} + if err := json.Unmarshal(responseBody, &result); err != nil { + return nil, fmt.Errorf("getting user payload from provider, could not parse response body with error: %v", err) + } + return result, nil +} diff --git a/user_payload_provider/http/provider_test.go b/user_payload_provider/http/provider_test.go new file mode 100644 index 00000000..ec7a5b82 --- /dev/null +++ b/user_payload_provider/http/provider_test.go @@ -0,0 +1,56 @@ +package http_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + hu "github.com/madappgang/identifo/user_payload_provider/http" +) + +const ( + secret = "super_secret" + appId = "12345" + appName = "I am the web app" + userId = "09876543d21" +) + +func Test_provider_TokenPayloadForApp(t *testing.T) { + //precalculated value from https://www.freeformatter.com/hmac-generator.html#ad-output + //using input: {"app_id":"12345","app_name":"I am the web app","user_id":"09876543d21"} + expectedDigest := "b9a6be00d9656fee55165749596a321a2c33abf795d61d5714c44715b81371a0" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + digest := r.Header["Digest"][0][len("SHA-256="):] + fmt.Println(digest) + if digest != expectedDigest { + t.Errorf("wrong digest %v, expected %v", digest, expectedDigest) + } + fmt.Fprintln(w, `{"city" : "Sydney"}`) + })) + defer ts.Close() + + p, err := hu.NewTokenPayloadProvider(secret, ts.URL) + if err != nil { + t.Errorf("unable to create provider with error %v", err) + t.FailNow() + } + + if p == nil { + t.Error("provider should not be empty") + t.FailNow() + } + + payload, err := p.TokenPayloadForApp(appId, appName, userId) + if err != nil { + t.Errorf("unable to get data payload with error %v", err) + t.FailNow() + } + + if payload["city"] != "Sydney" { + t.Errorf("Got unexpected value %v for key \"city\", expected \"Sydney\"", payload["city"]) + t.FailNow() + } + +} diff --git a/user_payload_provider/mock/provider.go b/user_payload_provider/mock/provider.go new file mode 100644 index 00000000..48078222 --- /dev/null +++ b/user_payload_provider/mock/provider.go @@ -0,0 +1,18 @@ +package mock + +import ( + "github.com/madappgang/identifo/model" +) + +func NewTokenPayloadProvider(payload map[string]interface{}) model.TokenPayloadProvider { + p := provider{payload: payload} + return &p +} + +type provider struct { + payload map[string]interface{} +} + +func (p *provider) TokenPayloadForApp(appId, appName, userId string) (map[string]interface{}, error) { + return p.payload, nil +} diff --git a/web/api/2fa.go b/web/api/2fa.go index b80c0ce2..80977cd8 100644 --- a/web/api/2fa.go +++ b/web/api/2fa.go @@ -156,8 +156,14 @@ func (ar *Router) FinalizeTFA() http.HandlerFunc { return } + tokenPayload, err := ar.getTokenPayloadForApp(app, user) + if err != nil { + ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "LoginWithPassword.loginUser") + return + } + offline := contains(scopes, jwtService.OfflineScope) - accessToken, refreshToken, err := ar.loginUser(user, d.Scopes, app, offline, false) + accessToken, refreshToken, err := ar.loginUser(user, d.Scopes, app, offline, false, tokenPayload) if err != nil { ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "FinalizeTFA.loginUser") return diff --git a/web/api/federated_login.go b/web/api/federated_login.go index 57d5f346..a2225d12 100644 --- a/web/api/federated_login.go +++ b/web/api/federated_login.go @@ -119,12 +119,21 @@ func (ar *Router) FederatedLogin() http.HandlerFunc { return } + // Request token payload + tokenPayload, err := ar.getTokenPayloadForApp(app, user) + if err != nil { + ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "LoginWithPassword.loginUser") + return + } + // Generate access token. - token, err := ar.tokenService.NewAccessToken(user, scopes, app, false) + token, err := ar.tokenService.NewAccessToken(user, scopes, app, false, tokenPayload) if err != nil { ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusUnauthorized, err.Error(), "FederatedLogin.tokenService_NewToken") return } + //check token payload data + tokenString, err := ar.tokenService.String(token) if err != nil { ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "FederatedLogin.tokenService_String") diff --git a/web/api/login.go b/web/api/login.go index d05e6b26..13ac68ce 100644 --- a/web/api/login.go +++ b/web/api/login.go @@ -8,6 +8,7 @@ import ( jwtService "github.com/madappgang/identifo/jwt/service" "github.com/madappgang/identifo/model" + thp "github.com/madappgang/identifo/user_payload_provider/http" "github.com/madappgang/identifo/web/authorization" "github.com/madappgang/identifo/web/middleware" "github.com/xlzd/gotp" @@ -105,7 +106,14 @@ func (ar *Router) LoginWithPassword() http.HandlerFunc { } offline := contains(scopes, jwtService.OfflineScope) - accessToken, refreshToken, err := ar.loginUser(user, scopes, app, offline, require2FA) + + tokenPayload, err := ar.getTokenPayloadForApp(app, user) + if err != nil { + ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "LoginWithPassword.loginUser") + return + } + + accessToken, refreshToken, err := ar.loginUser(user, scopes, app, offline, require2FA, tokenPayload) if err != nil { ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "LoginWithPassword.loginUser") return @@ -165,10 +173,31 @@ func (ar *Router) IsLoggedIn() http.HandlerFunc { } } +// getTokenPayloadForApp get additional token payload data +func (ar *Router) getTokenPayloadForApp(app model.AppData, user model.User) (map[string]interface{}, error) { + if app.TokenPayloadService() == model.TokenPayloadServiceHttp { + // check if we have service cached + ps, exists := ar.tokenPayloadServices[app.ID()] + if !exists { + var err error + ps, err = thp.NewTokenPayloadProvider( + app.TokenPayloadServiceHttpSettings().Secret, + app.TokenPayloadServiceHttpSettings().URL, + ) + if err != nil { + return nil, err + } + ar.tokenPayloadServices[app.ID()] = ps + } + return ps.TokenPayloadForApp(app.ID(), app.Name(), user.ID()) + } + return nil, nil +} + // loginUser creates and returns access token for a user. // createRefreshToken boolean param tells if we should issue refresh token as well. -func (ar *Router) loginUser(user model.User, scopes []string, app model.AppData, createRefreshToken, require2FA bool) (accessTokenString, refreshTokenString string, err error) { - token, err := ar.tokenService.NewAccessToken(user, scopes, app, require2FA) +func (ar *Router) loginUser(user model.User, scopes []string, app model.AppData, createRefreshToken, require2FA bool, tokenPayload map[string]interface{}) (accessTokenString, refreshTokenString string, err error) { + token, err := ar.tokenService.NewAccessToken(user, scopes, app, require2FA, tokenPayload) if err != nil { return } diff --git a/web/api/phone_login.go b/web/api/phone_login.go index 359d0ee7..27f1bf79 100644 --- a/web/api/phone_login.go +++ b/web/api/phone_login.go @@ -115,8 +115,14 @@ func (ar *Router) PhoneLogin() http.HandlerFunc { return } + tokenPayload, err := ar.getTokenPayloadForApp(app, user) + if err != nil { + ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "LoginWithPassword.loginUser") + return + } + offline := contains(scopes, jwtService.OfflineScope) - accessToken, refreshToken, err := ar.loginUser(user, scopes, app, offline, false) + accessToken, refreshToken, err := ar.loginUser(user, scopes, app, offline, false, tokenPayload) if err != nil { ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "PhoneLogin.loginUser") return diff --git a/web/api/registration.go b/web/api/registration.go index f897cace..32c95c89 100644 --- a/web/api/registration.go +++ b/web/api/registration.go @@ -108,7 +108,13 @@ func (ar *Router) RegisterWithPassword() http.HandlerFunc { return } - token, err := ar.tokenService.NewAccessToken(user, scopes, app, false) + tokenPayload, err := ar.getTokenPayloadForApp(app, user) + if err != nil { + ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "LoginWithPassword.loginUser") + return + } + + token, err := ar.tokenService.NewAccessToken(user, scopes, app, false, tokenPayload) if err != nil { ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusForbidden, err.Error(), "RegisterWithPassword.tokenService_NewToken") return diff --git a/web/api/router.go b/web/api/router.go index 05a59406..06e39d47 100644 --- a/web/api/router.go +++ b/web/api/router.go @@ -36,7 +36,8 @@ type Router struct { Authorizer *authorization.Authorizer Host string SupportedLoginWays model.LoginWith - WebRouterPrefix string + WebRouterPrefix string + tokenPayloadServices map[string]model.TokenPayloadProvider LoggerSettings model.LoggerSettings } @@ -126,6 +127,8 @@ func NewRouter(logger *log.Logger, as model.AppStorage, us model.UserStorage, ts ar.logger = log.New(os.Stdout, "API_ROUTER: ", log.Ldate|log.Ltime|log.Lshortfile) } + ar.tokenPayloadServices = make(map[string]model.TokenPayloadProvider) + if ar.cors != nil { ar.middleware.Use(ar.cors) } diff --git a/web/html/login.go b/web/html/login.go index 23b6f274..fd5c98fe 100644 --- a/web/html/login.go +++ b/web/html/login.go @@ -193,7 +193,7 @@ func (ar *Router) LoginHandler() http.HandlerFunc { } // TODO: Add TFA support. - token, err := ar.TokenService.NewAccessToken(user, scopes, app, false) + token, err := ar.TokenService.NewAccessToken(user, scopes, app, false, nil) if err != nil { ar.Logger.Printf("Error creating token: %v", err) serveTemplate() diff --git a/web/html/renew_token.go b/web/html/renew_token.go index 861c91f9..01e47d40 100644 --- a/web/html/renew_token.go +++ b/web/html/renew_token.go @@ -103,7 +103,7 @@ func (ar *Router) RenewToken() http.HandlerFunc { return } - token, err := ar.TokenService.NewAccessToken(user, scopes, app, false) + token, err := ar.TokenService.NewAccessToken(user, scopes, app, false, nil) if err != nil { ar.Logger.Printf("Error creating token: %v", err) serveTemplate("server error", "", redirectURI)