Skip to content

Commit

Permalink
feat: support jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
shaj13 committed Jan 12, 2021
1 parent f509a3b commit 65088eb
Show file tree
Hide file tree
Showing 14 changed files with 673 additions and 2 deletions.
100 changes: 100 additions & 0 deletions _examples/jwt/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2020 The Go-Guardian. All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.

package main

import (
"context"
"fmt"
"log"
"net/http"
"time"

gojwt "github.com/dgrijalva/jwt-go/v4"
"github.com/gorilla/mux"
"github.com/shaj13/libcache"
_ "github.com/shaj13/libcache/fifo"

"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/strategies/basic"
"github.com/shaj13/go-guardian/v2/auth/strategies/jwt"
"github.com/shaj13/go-guardian/v2/auth/strategies/union"
)

// Usage:
// curl -k http://127.0.0.1:8080/v1/book/1449311601 -u admin:admin
// curl -k http://127.0.0.1:8080/v1/auth/token -u admin:admin <obtain a token>
// curl -k http://127.0.0.1:8080/v1/book/1449311601 -H "Authorization: Bearer <token>"

var strategy union.Union
var keeper jwt.SecretsKeeper

func main() {
setupGoGuardian()
router := mux.NewRouter()
router.HandleFunc("/v1/auth/token", middleware(http.HandlerFunc(createToken))).Methods("GET")
router.HandleFunc("/v1/book/{id}", middleware(http.HandlerFunc(getBookAuthor))).Methods("GET")
log.Println("server started and listening on http://127.0.0.1:8080")
http.ListenAndServe("127.0.0.1:8080", router)
}

func createToken(w http.ResponseWriter, r *http.Request) {
u := auth.User(r)
token, _ := jwt.IssueAccessToken(u, keeper)
body := fmt.Sprintf("token: %s \n", token)
w.Write([]byte(body))
}

func getBookAuthor(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
books := map[string]string{
"1449311601": "Ryan Boyd",
"148425094X": "Yvonne Wilson",
"1484220498": "Prabath Siriwarden",
}
body := fmt.Sprintf("Author: %s \n", books[id])
w.Write([]byte(body))
}

func setupGoGuardian() {
keeper = jwt.StaticSecret{
ID: "secret-id",
Secret: []byte("secret"),
Method: gojwt.SigningMethodHS256,
}
cache := libcache.FIFO.New(0)
cache.SetTTL(time.Minute * 5)
cache.RegisterOnExpired(func(key, _ interface{}) {
cache.Peek(key)
})
basicStrategy := basic.NewCached(validateUser, cache)
jwtStrategy := jwt.New(cache, keeper)
strategy = union.New(jwtStrategy, basicStrategy)
}

func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
// here connect to db or any other service to fetch user and validate it.
if userName == "admin" && password == "admin" {
return auth.NewDefaultUser("admin", "1", nil, nil), nil
}

return nil, fmt.Errorf("Invalid credentials")
}

func middleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Executing Auth Middleware")
_, user, err := strategy.AuthenticateRequest(r)
if err != nil {
fmt.Println(err)
code := http.StatusUnauthorized
http.Error(w, http.StatusText(code), code)
return
}
log.Printf("User %s Authenticated\n", user.GetUserName())
r = auth.RequestWithUser(user, r)
next.ServeHTTP(w, r)
})
}
2 changes: 1 addition & 1 deletion auth/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func NewDefaultUser(name, id string, groups []string, extensions Extensions) *De

// NewUserInfo implements InfoConstructor and return Info object.
// Typically called from strategies to create a new user object when its authenticated.
func NewUserInfo(name, id string, groups []string, extensions map[string][]string) Info {
func NewUserInfo(name, id string, groups []string, extensions Extensions) Info {
if ic == nil {
return NewDefaultUser(name, id, groups, extensions)
}
Expand Down
36 changes: 36 additions & 0 deletions auth/strategies/jwt/claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package jwt

import (
"time"

"github.com/dgrijalva/jwt-go/v4"

"github.com/shaj13/go-guardian/v2/auth"
)

type claims struct {
UserInfo auth.Info `json:"info"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience jwt.ClaimStrings `json:"aud"`
Expiration time.Time `json:"exp"`
NotBefore time.Time `json:"nbf"`
IssuedAt time.Time `json:"iat"`
}

// nolint:govet
func (c claims) Valid(v *jwt.ValidationHelper) error {
if err := v.ValidateAudience(c.Audience); err != nil {
return err
}

if err := v.ValidateIssuer(c.Issuer); err != nil {
return err
}

if err := v.ValidateNotBefore(&jwt.Time{c.NotBefore}); err != nil {
return err
}

return v.ValidateExpiresAt(&jwt.Time{c.Expiration})
}
64 changes: 64 additions & 0 deletions auth/strategies/jwt/claims_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package jwt

import (
"testing"
"time"

"github.com/dgrijalva/jwt-go/v4"
"github.com/stretchr/testify/assert"
)

func TestClaimsValid(t *testing.T) {
table := []struct {
name string
c claims
opt jwt.ParserOption
expectErr bool
}{
{
name: "it return nil when claims valid",
c: claims{
Expiration: time.Now().Add(time.Hour),
},
opt: jwt.WithoutClaimsValidation(),
},
{
name: "it return error when claims expired",
expectErr: true,
c: claims{},
opt: jwt.WithoutClaimsValidation(),
},
{
name: "it return error when token not valid nbf",
expectErr: true,
c: claims{
NotBefore: time.Now().Add(time.Hour),
},
opt: jwt.WithoutClaimsValidation(),
},
{
name: "it return error when token not valid aud",
expectErr: true,
c: claims{
Audience: jwt.ClaimStrings{"test"},
},
opt: jwt.WithAudience("#test#"),
},
{
name: "it return error when token not valid iss",
expectErr: true,
c: claims{
Issuer: "test",
},
opt: jwt.WithIssuer("#test#"),
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
vh := jwt.NewValidationHelper(tt.opt)
err := tt.c.Valid(vh)
assert.Equal(t, tt.expectErr, err != nil)
})
}
}
118 changes: 118 additions & 0 deletions auth/strategies/jwt/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package jwt_test

import (
"fmt"
"net/http"
"time"

"github.com/shaj13/go-guardian/v2/auth"

gojwt "github.com/dgrijalva/jwt-go/v4"
"github.com/shaj13/libcache"
_ "github.com/shaj13/libcache/lru"

"github.com/shaj13/go-guardian/v2/auth/strategies/jwt"
)

type RotatedSecrets struct {
Secrtes map[string][]byte
LatestID string
RotationDuration time.Duration
LastRotation time.Time
}

func (r RotatedSecrets) KID() string {
if time.Now().After(r.LastRotation) {
r.LastRotation = time.Now().Add(r.RotationDuration)
r.LatestID = "your generated id"
r.Secrtes[r.LatestID] = []byte("your generated secrets")
}
return r.LatestID
}

func (r RotatedSecrets) Get(kid string) (key interface{}, m gojwt.SigningMethod, err error) {
s, ok := r.Secrtes[kid]
if ok {
return s, gojwt.SigningMethodHS256, nil
}
return nil, nil, fmt.Errorf("Invalid KID %s", kid)
}

func Example() {
u := auth.NewUserInfo("example", "example", nil, nil)
c := libcache.LRU.New(0)
s := jwt.StaticSecret{
ID: "id",
Method: gojwt.SigningMethodHS256,
Secret: []byte("your secret"),
}

token, err := jwt.IssueAccessToken(u, s)
strategy := jwt.New(c, s)

fmt.Println(err)

// user request
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer "+token)
user, err := strategy.Authenticate(r.Context(), r)
fmt.Println(user.GetID(), err)

// Output:
// <nil>
// example <nil>
}

func ExampleSecretsKeeper() {
// The example shows how to create your custom secrets keeper to rotate secrets.
s := RotatedSecrets{
Secrtes: make(map[string][]byte),
}
u := auth.NewUserInfo("example", "example", nil, nil)
c := libcache.LRU.New(0)

token, err := jwt.IssueAccessToken(u, s)
strategy := jwt.New(c, s)

fmt.Println(err)

// user request
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer "+token)
user, err := strategy.Authenticate(r.Context(), r)
fmt.Println(user.GetID(), err)

// Output:
// <nil>
// example <nil>
}

func ExampleSetAudience() {
aud := jwt.SetAudience("example-aud")
u := auth.NewUserInfo("example", "example", nil, nil)
s := jwt.StaticSecret{}
c := libcache.LRU.New(0)

_, _ = jwt.IssueAccessToken(u, s, aud)
_ = jwt.New(c, s, aud)
}

func ExampleSetIssuer() {
iss := jwt.SetIssuer("example-iss")
u := auth.NewUserInfo("example", "example", nil, nil)
s := jwt.StaticSecret{}
c := libcache.LRU.New(0)

_, _ = jwt.IssueAccessToken(u, s, iss)
_ = jwt.New(c, s, iss)
}

func ExampleSetExpDuration() {
exp := jwt.SetExpDuration(time.Hour)
u := auth.NewUserInfo("example", "example", nil, nil)
s := jwt.StaticSecret{}
c := libcache.LRU.New(0)

_, _ = jwt.IssueAccessToken(u, s, exp)
_ = jwt.New(c, s, exp)
}
29 changes: 29 additions & 0 deletions auth/strategies/jwt/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Package jwt provides authentication strategy,
// to authenticate HTTP requests based on jwt token.
package jwt

import (
"context"
"net/http"
"time"

"github.com/shaj13/go-guardian/v2/auth"
"github.com/shaj13/go-guardian/v2/auth/strategies/token"
)

// GetAuthenticateFunc return function to authenticate request using jwt token.
// The returned function typically used with the token strategy.
func GetAuthenticateFunc(s SecretsKeeper, opts ...auth.Option) token.AuthenticateFunc {
t := newAccessToken(s, opts...)
return func(ctx context.Context, r *http.Request, token string) (auth.Info, time.Time, error) {
c, err := t.parse(token)
return c.UserInfo, c.Expiration, err
}
}

// New return strategy authenticate request using jwt token.
// New is similar to token.New().
func New(c auth.Cache, s SecretsKeeper, opts ...auth.Option) auth.Strategy {
fn := GetAuthenticateFunc(s, opts...)
return token.New(fn, c, opts...)
}
Loading

0 comments on commit 65088eb

Please sign in to comment.