/
credentials.go
164 lines (133 loc) · 4 KB
/
credentials.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
package auth
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/SKF/go-rest-utility/client/retry"
)
var (
ErrIncorrectCredentials = errors.New("incorrect credentials")
ErrChallenged = errors.New("user password needs to be reset")
ErrTooManyRequests = errors.New("too many requests to Enlight SSO")
ErrInactivated = errors.New("user has been inactivated")
ErrUnknownTokenType = errors.New("provided token type not present in response")
)
const (
DefaultTokenType = "identityToken"
)
type CredentialsTokenProvider struct {
Username string
Password string
Endpoint string
Client CredentialsClient
TokenType string
Retry retry.BackoffProvider
}
type CredentialsClient interface {
Do(*http.Request) (*http.Response, error)
}
type SignInRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type SignInResponse struct {
Tokens map[string]RawToken `json:"tokens"`
Challenge *struct {
ID string `json:"id"`
Type string `json:"type"`
} `json:"challenge"`
}
func (provider *CredentialsTokenProvider) GetRawToken(ctx context.Context) (RawToken, error) {
if provider.TokenType == "" {
provider.TokenType = DefaultTokenType
}
signIn := provider.signIn
if provider.Retry != nil {
signIn = provider.signInWithRetry
}
response, err := signIn(ctx, SignInRequest{
Username: provider.Username,
Password: provider.Password,
})
if err != nil {
return "", fmt.Errorf("failed to sign-in: %w", err)
}
if response.Challenge != nil {
return "", ErrChallenged
}
token := response.Tokens[provider.TokenType]
if token == "" {
return "", fmt.Errorf("%w: %s", ErrUnknownTokenType, provider.TokenType)
}
return token, nil
}
func (provider *CredentialsTokenProvider) signInWithRetry(ctx context.Context, creds SignInRequest) (*SignInResponse, error) {
for attempt := 1; ; attempt++ {
response, err := provider.signIn(ctx, creds)
if err == nil || errors.Is(err, ErrIncorrectCredentials) || errors.Is(err, ErrInactivated) {
return response, err
}
backoff, backoffErr := provider.Retry.BackoffByAttempt(attempt)
if backoffErr != nil {
if errors.Is(backoffErr, retry.ErrBackoffExhausted) {
return response, err
}
return response, fmt.Errorf("failed generating retry backoff: %w", backoffErr)
}
time.Sleep(backoff)
}
}
func (provider *CredentialsTokenProvider) signIn(ctx context.Context, creds SignInRequest) (*SignInResponse, error) {
if provider.Client == nil {
provider.Client = http.DefaultClient
}
payload, err := json.Marshal(creds)
if err != nil {
return nil, fmt.Errorf("marshalling credentials: %w", err)
}
r, err := http.NewRequestWithContext(ctx, http.MethodPost, provider.Endpoint, bytes.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("initializing http request: %w", err)
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
rs, err := provider.Client.Do(r)
if err != nil {
return nil, fmt.Errorf("failed to perform http request: %w", err)
}
defer rs.Body.Close()
if ct := rs.Header.Get("Content-Type"); ct != "application/json" {
body, err := io.ReadAll(rs.Body)
if err != nil {
return nil, fmt.Errorf("failed reading non json response: %w", err)
}
return nil, fmt.Errorf("unexpected content-type: %s %d: %s", ct, rs.StatusCode, body)
}
var response struct {
Data SignInResponse
Error struct {
Message string
}
}
if err := json.NewDecoder(rs.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("unmarshalling json response: %w", err)
}
switch rs.StatusCode {
case http.StatusOK:
return &response.Data, nil
case http.StatusBadRequest:
if response.Error.Message == "incorrect username or password" {
return nil, ErrIncorrectCredentials
}
case http.StatusConflict:
return nil, ErrInactivated
case http.StatusTooManyRequests:
return nil, ErrTooManyRequests
}
return nil, fmt.Errorf("unknown http error %d: %s", rs.StatusCode, response.Error.Message)
}