/
onboardingmodule.go
160 lines (134 loc) · 5.22 KB
/
onboardingmodule.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
package keycloakb
import (
"context"
"crypto/rand"
b64 "encoding/base64"
"encoding/json"
"math/big"
"net/http"
"net/url"
"regexp"
"strings"
"time"
errorhandler "github.com/cloudtrust/common-service/errors"
"github.com/cloudtrust/common-service/log"
"github.com/cloudtrust/keycloak-bridge/internal/constants"
kc "github.com/cloudtrust/keycloak-client"
)
// TrustIDAuthToken struct
type TrustIDAuthToken struct {
Token string `json:"token"`
CreatedAt int64 `json:"created_at"`
}
// ToJSON converts TrustIDAuthToken to its JSON representation
func (t TrustIDAuthToken) ToJSON() string {
var authBytes, _ = json.Marshal(t)
return string(authBytes)
}
type onboardingModule struct {
keycloakClient OnboardingKeycloakClient
keycloakURL string
logger log.Logger
}
// OnboardingKeycloakClient interface
type OnboardingKeycloakClient interface {
CreateUser(accessToken string, realmName string, targetRealmName string, user kc.UserRepresentation) (string, error)
ExecuteActionsEmail(accessToken string, realmName string, userID string, actions []string, paramKV ...string) error
}
//OnboardingModule interface
type OnboardingModule interface {
GenerateAuthToken() (TrustIDAuthToken, error)
OnboardingAlreadyCompleted(kc.UserRepresentation) (bool, error)
SendOnboardingEmail(ctx context.Context, accessToken string, realmName string, userID string,
username string, autoLoginToken TrustIDAuthToken, onboardingClientID string, onboardingRedirectURI string) error
CreateUser(ctx context.Context, accessToken, realmName, targetRealmName string, kcUser *kc.UserRepresentation) (string, error)
}
// NewOnboardingModule creates an onboarding module
func NewOnboardingModule(keycloakClient OnboardingKeycloakClient, keycloakURL string, logger log.Logger) OnboardingModule {
return &onboardingModule{
keycloakClient: keycloakClient,
keycloakURL: keycloakURL,
logger: logger,
}
}
// GenerateAuthToken generates a random AUTO_LOGIN_TOKEN, used to perform auto login at the end of the onboarding process
func (om *onboardingModule) GenerateAuthToken() (TrustIDAuthToken, error) {
var bToken = make([]byte, 32)
_, err := rand.Read(bToken)
if err != nil {
return TrustIDAuthToken{}, err
}
return TrustIDAuthToken{
Token: b64.StdEncoding.EncodeToString(bToken),
CreatedAt: time.Now().Unix(),
}, nil
}
// OnboardingAlreadyCompleted checks if the onboarding process has already been performed
func (om *onboardingModule) OnboardingAlreadyCompleted(kcUser kc.UserRepresentation) (bool, error) {
onboardingCompleted, err := kcUser.GetAttributeBool(constants.AttrbOnboardingCompleted)
if err != nil {
return false, err
}
return onboardingCompleted != nil && *onboardingCompleted, nil
}
func (om *onboardingModule) SendOnboardingEmail(ctx context.Context, accessToken string, realmName string, userID string,
username string, autoLoginToken TrustIDAuthToken, onboardingClientID string, onboardingRedirectURI string) error {
redirectURL, err := url.Parse(om.keycloakURL + "/auth/realms/" + realmName + "/protocol/openid-connect/auth")
if err != nil {
om.logger.Warn(ctx, "msg", "Can't parse keycloak URL", "err", err.Error())
return err
}
var parameters = url.Values{}
parameters.Add("client_id", onboardingClientID)
parameters.Add("scope", "openid")
parameters.Add("response_type", "code")
parameters.Add("trustid_auth_token", autoLoginToken.Token)
parameters.Add("redirect_uri", onboardingRedirectURI)
parameters.Add("login_hint", username)
redirectURL.RawQuery = parameters.Encode()
err = om.keycloakClient.ExecuteActionsEmail(accessToken, realmName, userID, []string{"VERIFY_EMAIL"}, "client_id", onboardingClientID, "redirect_uri", redirectURL.String())
if err != nil {
om.logger.Warn(ctx, "msg", "ExecuteActionsEmail failed", "err", err.Error())
return err
}
return nil
}
func (om *onboardingModule) generateUsername(chars []rune, length int) string {
var b strings.Builder
for j := 0; j < length; j++ {
nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
index := int(nBig.Int64())
b.WriteRune(chars[index])
}
return b.String()
}
func (om *onboardingModule) CreateUser(ctx context.Context, accessToken, realmName, targetRealmName string, kcUser *kc.UserRepresentation) (string, error) {
var chars = []rune("0123456789")
var locationURL string
var username string
var err error
for i := 0; i < 10; i++ {
username = om.generateUsername(chars, 8)
kcUser.Username = &username
locationURL, err = om.keycloakClient.CreateUser(accessToken, realmName, targetRealmName, *kcUser)
// Create success: just have to get the userID and exit this loop
if err == nil {
var re = regexp.MustCompile(`(^.*/users/)`)
var userID = re.ReplaceAllString(locationURL, "")
kcUser.ID = &userID
return locationURL, nil
}
kcUser.Username = nil
switch e := err.(type) {
case errorhandler.Error:
if e.Status == http.StatusConflict && e.Message == "keycloak.existing.username" {
// Username already exists
continue
}
}
om.logger.Warn(ctx, "msg", "Failed to create user through Keycloak API", "err", err.Error())
return "", err
}
om.logger.Warn(ctx, "msg", "Can't generate unused username after multiple attempts")
return "", errorhandler.CreateInternalServerError("username.generation")
}