Skip to content

Commit

Permalink
[1126] section 84
Browse files Browse the repository at this point in the history
  • Loading branch information
myeunee committed Nov 26, 2024
1 parent b11b5ec commit 3960b55
Show file tree
Hide file tree
Showing 28 changed files with 1,270 additions and 5 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ jobs:
MYSQL_DATABASE: todo
MYSQL_USER: todo
MYSQL_PASSWORD: todo
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/setup-go@v3
with:
Expand Down
Binary file removed chapter19/section75/tmp/main
Binary file not shown.
14 changes: 14 additions & 0 deletions chapter20/auth/cert/public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhj1+/tBQ2DpTr/xQCrch
3Gg5zfrOcQPeg4U+StPLInp52p5WQnEliX0sT7/YqJFTCR9z7Jxvli4/1K6IfU66
b8c+s7+r8JxMYGPhM5ef3TZ7EWYAgK2ZrlFtuo7oUwPPtV19iAhlUEWk1WKNQa7t
Pl9EjFcAi6EuDMduBxYqd2u3MCEsN45rVxBDw8byGTFWSLGvxnxw1w6cYQNkcz9d
+Dte5SEa4UwCxdrQ3kNlNwqC9D5qCm/teSBQdETO5DiLGB0HuNXIII51wFx5rfLw
MyPQhwxP1/Dnua2TKVi5flkDLFWBaA+OynFZt9yTrE1IBVpWLMM6UZz/FAM+1s1l
T33gZDQmWiuwLlla2+rwWmj7R/QfTB+xRt5zMqDNvcOvwDpJGlxwN0kFUMryQAJN
ueXRdr+7V/hJXC8SCWX3O1lM/v7MDKD7uOzXCHiWIYN1A6CwpvII6IvJgaE91FFK
HIMysMz/jeckNANpzCMZWe+kNAHHBbgoUIqgw/E0aoV8IkPmtc+xPG09hJloLPC+
CrmFFXcipbKAk1ri0lf3o20SG8KiYI/LWx78TAnwzNHXx+5Fq4G3RB0mlw2Lj3iX
A0LBlFlGPyndpmhVre6jQP/sMCgHk9jxb0B6zuDLOHGSSv0VyxYrDObg2OHb5brk
OSYL30MQGa7EAbGcXClwh10CAwEAAQ==
-----END PUBLIC KEY-----
52 changes: 52 additions & 0 deletions chapter20/auth/cert/secret.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
+YwS5CpVbeyJPK7beyCM3lBi2R0XYQ==
-----END PRIVATE KEY-----
155 changes: 155 additions & 0 deletions chapter20/auth/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package auth

import (
"context"
_ "embed"
"fmt"
"net/http"
"time"

"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"
"github.com/myeunee/GolangStudy/chapter20/clock"
"github.com/myeunee/GolangStudy/chapter20/entity"
)

const (
RoleKey = "role"
UserNameKey = "user_name"
)

//go:embed cert/secret.pem
var rawPrivKey []byte

//go:embed cert/public.pem
var rawPubKey []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}
privkey, err := parse(rawPrivKey)
if err != nil {
return nil, fmt.Errorf("failed in NewJWTer: private key: %w", err)
}
pubkey, err := parse(rawPubKey)
if err != nil {
return nil, fmt.Errorf("failed in NewJWTer: public key: %w", err)
}
j.PrivateKey = privkey
j.PublicKey = pubkey
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) {
tok, err := jwt.NewBuilder().
JwtID(uuid.New().String()).
Issuer(`github.com/myeunee/GolangStudy`).
Subject("access_token").
IssuedAt(j.Clocker.Now()).
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, tok.JwtID(), u.ID); err != nil {
return nil, err
}

// Sign a JWT
signed, err := jwt.Sign(tok, 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)
}
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)
clone := r.Clone(ctx)
return clone, 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, tok jwt.Token) context.Context {
get, ok := tok.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"
}
Loading

0 comments on commit 3960b55

Please sign in to comment.