Repository navigation
Expand file tree
/
Copy pathcredential.go
More file actions
105 lines (94 loc) · 3.28 KB
/
Copy pathcredential.go
File metadata and controls
105 lines (94 loc) · 3.28 KB
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
package identity
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"time"
)
// SecretPrefix tags an authkit machine credential so callers can tell it apart
// from other tokens (e.g. an admin token).
const SecretPrefix = "ak_"
var (
ErrInvalidCredential = errors.New("invalid credential")
ErrRevoked = errors.New("credential revoked")
ErrWrongSubject = errors.New("credential bound to a different subject")
ErrNoSigner = errors.New("no signer configured")
)
// GenerateSecret returns a fresh high-entropy credential secret.
func GenerateSecret() string {
b := make([]byte, 32)
_, _ = rand.Read(b)
return SecretPrefix + base64.RawURLEncoding.EncodeToString(b)
}
// NewID returns a random identifier for a credential row.
func NewID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return "cred_" + base64.RawURLEncoding.EncodeToString(b)
}
// CredentialService mints, exchanges, and revokes durable machine credentials.
// Exchange is the only store-backed auth step; everything after runs on the
// stateless JWT the exchange returns.
type CredentialService struct {
store CredentialStore
signer Signer
defaultTTL time.Duration
}
// NewCredentialService builds the service. ttl ≤ 0 defaults to 10 minutes.
func NewCredentialService(store CredentialStore, signer Signer, ttl time.Duration) *CredentialService {
if ttl <= 0 {
ttl = 10 * time.Minute
}
return &CredentialService{store: store, signer: signer, defaultTTL: ttl}
}
// Mint creates a new credential for an organization and returns the plaintext
// secret exactly once (only its hash is stored).
func (s *CredentialService) Mint(ctx context.Context, orgID, name string, scopes []string) (string, Credential, error) {
secret := GenerateSecret()
cred := Credential{
ID: NewID(),
OrgID: orgID,
Name: name,
SecretHash: HashSecret(secret),
Scopes: scopes,
CreatedAt: time.Now().UTC(),
}
if err := s.store.Create(ctx, cred); err != nil {
return "", Credential{}, err
}
return secret, cred, nil
}
// Exchange validates a presented secret, binds it to subject on first use, and
// mints a short-lived JWT carrying the credential's org + scopes.
func (s *CredentialService) Exchange(ctx context.Context, secret, subject string, ttl time.Duration) (string, time.Time, error) {
if s.signer == nil {
return "", time.Time{}, ErrNoSigner
}
cred, err := s.store.GetByHash(ctx, HashSecret(secret))
if err != nil {
return "", time.Time{}, ErrInvalidCredential
}
if cred.Revoked() {
return "", time.Time{}, ErrRevoked
}
if cred.Subject == "" {
if err := s.store.Bind(ctx, cred.ID, subject); err != nil {
return "", time.Time{}, err
}
cred.Subject = subject
} else if cred.Subject != subject {
return "", time.Time{}, ErrWrongSubject
}
_ = s.store.Touch(ctx, cred.ID)
if ttl <= 0 {
ttl = s.defaultTTL
}
return s.signer.Mint(Claims{Subject: cred.Subject, OrgID: cred.OrgID, Scopes: cred.Scopes}, ttl)
}
// Revoke disables a credential by id.
func (s *CredentialService) Revoke(ctx context.Context, id string) error { return s.store.Revoke(ctx, id) }
// List returns an organization's credentials (hashes omitted by the json tag).
func (s *CredentialService) List(ctx context.Context, orgID string) ([]Credential, error) {
return s.store.List(ctx, orgID)
}