-
Notifications
You must be signed in to change notification settings - Fork 1
/
token.go
298 lines (263 loc) · 9 KB
/
token.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
package security
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/http/httptest"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/ed25519"
"github.com/gorilla/mux"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
)
// TokenCfg contains the data for filling the token
type TokenCfg struct {
Alg jose.SignatureAlgorithm
KeyBitlength int
IssuerUrl string
Audience []string
ExpiresAt time.Time
IssuedAt time.Time
Id string
Subject string
Name string
PreferredName string
Email string
Roles []string
}
const (
//nolint:gosec
defaultTokenIssuerURL = "https://oidc.metal-stack.io"
//nolint:gosec
defaultTokenSubject = "AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4"
defaultTokenClientID = "metal-stack"
defaultTokenName = "Achim Admin"
//nolint:gosec
defaultTokenEMail = "[email protected]"
defaultTokenPreferredName = "xyz4711"
)
// DefaultTokenCfg creates a TokenCfg filled with default values
func DefaultTokenCfg() *TokenCfg {
return &TokenCfg{
Alg: jose.RS256,
KeyBitlength: 0, // use default, i.e. 2048 for RSA
IssuerUrl: defaultTokenIssuerURL,
Audience: []string{defaultTokenClientID},
ExpiresAt: time.Now().Add(5 * time.Minute),
IssuedAt: time.Now(),
Id: "123",
Subject: defaultTokenSubject,
Name: defaultTokenName,
PreferredName: defaultTokenPreferredName,
Email: defaultTokenEMail,
Roles: []string{"Tn_k8s-all-all-cadm"},
}
}
// MustCreateTokenAndKeys creates a keyset and token, panics on error
func MustCreateTokenAndKeys(cfg *TokenCfg) (token string, pubKey jose.JSONWebKey, privKey jose.JSONWebKey) {
token, pubKey, privKey, err := CreateTokenAndKeys(cfg)
if err != nil {
panic(err)
}
return token, pubKey, privKey
}
// CreateTokenAndKeys creates a keyset and token
func CreateTokenAndKeys(cfg *TokenCfg) (token string, pubKey jose.JSONWebKey, privKey jose.JSONWebKey, err error) {
pubKey, privKey, err = CreateWebkeyPair(cfg.Alg, "sig", cfg.KeyBitlength)
if err != nil {
return "", jose.JSONWebKey{}, jose.JSONWebKey{}, err
}
cl := jwt.Claims{
Issuer: cfg.IssuerUrl,
Subject: cfg.Subject,
Audience: cfg.Audience,
Expiry: jwt.NewNumericDate(cfg.ExpiresAt),
NotBefore: jwt.NewNumericDate(cfg.IssuedAt),
IssuedAt: jwt.NewNumericDate(cfg.IssuedAt),
ID: cfg.Id,
}
pcl := GenericOIDCClaims{
Name: cfg.Name,
PreferredUsername: cfg.PreferredName,
EMail: cfg.Email,
Roles: cfg.Roles,
}
signer := MustMakeSigner(cfg.Alg, privKey)
token, err = CreateToken(signer, cl, pcl)
if err != nil {
return "", jose.JSONWebKey{}, jose.JSONWebKey{}, err
}
return token, pubKey, privKey, nil
}
// TokenProvider creates the token with the given TokenCfg
type TokenProvider func(cfg *TokenCfg) (string, jose.JSONWebKey, jose.JSONWebKey)
type KeyServerConfig struct {
keyResponseDelay time.Duration
}
type KeyServerOption func(cfg *KeyServerConfig)
func KeyResponseTimeDelay(delay time.Duration) KeyServerOption {
return func(cfg *KeyServerConfig) {
cfg.keyResponseDelay = delay
}
}
// GenerateTokenAndKeyServer starts keyserver, patches tokenCfg (issuer), generates token.
// This method is intended for test purposes, where you need a server that provides
// '.well-known/openid-configuration' and '/keys' endpoints.
func GenerateTokenAndKeyServer(tc *TokenCfg, tokenProvider TokenProvider, opts ...KeyServerOption) (srv *httptest.Server, token string, err error) {
cfg := &KeyServerConfig{}
for _, o := range opts {
o(cfg)
}
var issuer string
var pubKey jose.JSONWebKey
mx := mux.NewRouter()
// start test-http-server, the local address will be the issuer-url for our token
srv = httptest.NewServer(mx)
// closure: used by fake-oidc-key-server below
issuer = srv.URL
// patch TokenCfg with local issuer
tc.IssuerUrl = issuer
// generate token
token, pubKey, _ = tokenProvider(tc)
// enable oidc discovery
mx.HandleFunc("/.well-known/openid-configuration", func(writer http.ResponseWriter, request *http.Request) {
p := providerJSON{
Issuer: issuer,
JWKSURL: issuer + "/keys",
Algorithms: []jose.SignatureAlgorithm{tc.Alg},
}
err := json.NewEncoder(writer).Encode(p)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
}
})
// key-endpoint to obtain public-key for token validation
mx.HandleFunc("/keys", func(writer http.ResponseWriter, request *http.Request) {
ks := jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{pubKey},
}
if cfg.keyResponseDelay > 0 {
time.Sleep(cfg.keyResponseDelay)
}
err := json.NewEncoder(writer).Encode(ks)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
}
})
return srv, token, nil
}
// providerJSON is the response struct for the .well-known/openid-configuration endpoint
type providerJSON struct {
Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
UserInfoURL string `json:"userinfo_endpoint"`
Algorithms []jose.SignatureAlgorithm `json:"id_token_signing_alg_values_supported"`
}
// CreateToken creates a jwt token with the given claims
func CreateToken(signer jose.Signer, cl interface{}, privateClaims ...interface{}) (string, error) {
builder := jwt.Signed(signer).Claims(cl)
for i := range privateClaims {
builder = builder.Claims(privateClaims[i])
}
raw, err := builder.Serialize()
if err != nil {
return "", err
}
return raw, nil
}
// MustMakeSigner creates a Signer and panics if an error occurs
func MustMakeSigner(alg jose.SignatureAlgorithm, k interface{}) jose.Signer {
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: k}, nil)
if err != nil {
panic("failed to create signer:" + err.Error())
}
return sig
}
// CreateWebkeyPair creates a JSONWebKey-Pair.
// alg is one of jose signature-algorithm constants, e.g. jose.RS256.
// use is "sig" for signature or "enc" for encryption, see https://tools.ietf.org/html/rfc7517#page-6
// Arbitrary keylenBits are not supported for Elliptic Curve Algs, here the Bits must match the Algorithms.
func CreateWebkeyPair(alg jose.SignatureAlgorithm, use string, keylenBits int) (jose.JSONWebKey, jose.JSONWebKey, error) {
kid := uuid.New().String()
var publicKey crypto.PrivateKey
var privateKey crypto.PublicKey
var err error
publicKey, privateKey, err = GenerateSigningKey(alg, keylenBits)
if err != nil {
return jose.JSONWebKey{}, jose.JSONWebKey{}, err
}
salg := string(alg)
publicWebKey := jose.JSONWebKey{Key: publicKey, KeyID: kid, Algorithm: salg, Use: use}
privateWebKey := jose.JSONWebKey{Key: privateKey, KeyID: kid, Algorithm: salg, Use: use}
if privateWebKey.IsPublic() || !publicWebKey.IsPublic() || !privateWebKey.Valid() || !publicWebKey.Valid() {
log.Fatalf("invalid keys were generated")
}
return publicWebKey, privateWebKey, nil
}
// GenerateSigningKey generates a keypair for corresponding SignatureAlgorithm.
func GenerateSigningKey(alg jose.SignatureAlgorithm, bits int) (crypto.PublicKey, crypto.PrivateKey, error) {
switch alg {
case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA:
keylen := map[jose.SignatureAlgorithm]int{
jose.ES256: 256,
jose.ES384: 384,
jose.ES512: 521, // The NIST P-521 named curve has an order (n) length of 521 bits, this is not a typo.
jose.EdDSA: 256,
}
if bits != 0 && bits != keylen[alg] {
return nil, nil, errors.New("invalid elliptic curve key size, this algorithm does not support arbitrary size")
}
case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512:
if bits == 0 {
bits = 2048
}
if bits < 2048 {
return nil, nil, errors.New("invalid key size for RSA key, 2048 or more is required")
}
case jose.HS256, jose.HS384, jose.HS512:
return nil, nil, fmt.Errorf("unsupported algorithm %s for signing key", alg)
}
switch alg {
case jose.ES256:
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
return key.Public(), key, err
case jose.ES384:
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, nil, err
}
return key.Public(), key, err
case jose.ES512:
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
return nil, nil, err
}
return key.Public(), key, err
case jose.EdDSA:
pub, key, err := ed25519.GenerateKey(rand.Reader)
return pub, key, err
case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512:
key, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return key.Public(), key, err
case jose.HS256, jose.HS384, jose.HS512:
return nil, nil, fmt.Errorf("unsupported algorithm %s for signing key", alg)
default:
return nil, nil, fmt.Errorf("unknown algorithm %s for signing key", alg)
}
}