-
Notifications
You must be signed in to change notification settings - Fork 0
/
jwt.go
157 lines (134 loc) · 3.82 KB
/
jwt.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
package auth
import (
"context"
_ "embed"
"fmt"
"net/http"
"time"
"github.com/ashtkn/go_todo_app/clock"
"github.com/ashtkn/go_todo_app/entity"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
)
const (
RoleKey = "role"
UserNameKey = "user_name"
)
//go:embed cert/secret.pem
var rawPrivateKey []byte
//go:embed cert/public.pem
var rawPublicKey []byte
type JWTer struct {
PrivateKey, PublicKey jwk.Key
Store Store
Clocker clock.Clocker
}
//go:generate go run github.com/matryer/moq -out moq_test.go . Store
type Store interface {
Save(ctx context.Context, key string, userId entity.UserID) error
Load(ctx context.Context, key string) (entity.UserID, error)
}
func NewJWTer(s Store, c clock.Clocker) (*JWTer, error) {
j := &JWTer{Store: s}
privateKey, err := parse(rawPrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
publicKey, err := parse(rawPublicKey)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
j.PrivateKey = privateKey
j.PublicKey = publicKey
j.Clocker = c
return j, nil
}
func parse(rawKey []byte) (jwk.Key, error) {
key, err := jwk.ParseKey(rawKey, jwk.WithPEM(true))
if err != nil {
return nil, err
}
return key, nil
}
func (j *JWTer) GenerateToken(ctx context.Context, u entity.User) ([]byte, error) {
token, err := jwt.NewBuilder().
JwtID(uuid.New().String()).
Issuer(`github.com/ashtkn/go_todo_app`).
Subject("access_token").
IssuedAt(j.Clocker.Now()).
// redisのexpireはこれを使う。
// https://pkg.go.dev/github.com/go-redis/redis/v8#Client.Set
// clock.Durationだから Subする必要がある
Expiration(j.Clocker.Now().Add(30*time.Minute)).
Claim(RoleKey, u.Role).
Claim(UserNameKey, u.Name).
Build()
if err != nil {
return nil, fmt.Errorf("GenerateToken: failed to build token: %w", err)
}
if err := j.Store.Save(ctx, token.JwtID(), u.ID); err != nil {
return nil, err
}
// Sign a JWT!
signed, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, j.PrivateKey))
if err != nil {
return nil, err
}
return signed, nil
}
func (j *JWTer) GetToken(ctx context.Context, r *http.Request) (jwt.Token, error) {
token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, j.PublicKey), jwt.WithValidate(false))
if err != nil {
return nil, err
}
if err := jwt.Validate(token, jwt.WithClock(j.Clocker)); err != nil {
return nil, fmt.Errorf("GetToken: failed to validate token: %w", err)
}
// Redisから削除して手動でexpireさせていることもありうる
if _, err := j.Store.Load(ctx, token.JwtID()); err != nil {
return nil, fmt.Errorf("GetToken: %q expired: %w", token.JwtID(), err)
}
return token, nil
}
type userIDKey struct{}
type roleKey struct{}
func (j *JWTer) FillContext(r *http.Request) (*http.Request, error) {
token, err := j.GetToken(r.Context(), r)
if err != nil {
return nil, err
}
uid, err := j.Store.Load(r.Context(), token.JwtID())
if err != nil {
return nil, err
}
ctx := SetUserID(r.Context(), uid)
ctx = SetRole(ctx, token)
return r.Clone(ctx), nil
}
func SetUserID(ctx context.Context, uid entity.UserID) context.Context {
return context.WithValue(ctx, userIDKey{}, uid)
}
func GetUserID(ctx context.Context) (entity.UserID, bool) {
id, ok := ctx.Value(userIDKey{}).(entity.UserID)
return id, ok
}
func SetRole(ctx context.Context, token jwt.Token) context.Context {
get, ok := token.Get(RoleKey)
if !ok {
return context.WithValue(ctx, roleKey{}, "")
}
return context.WithValue(ctx, roleKey{}, get)
}
func GetRole(ctx context.Context) (string, bool) {
role, ok := ctx.Value(roleKey{}).(string)
return role, ok
}
func IsAdmin(ctx context.Context) bool {
role, ok := GetRole(ctx)
if !ok {
return false
}
return role == "admin"
}