Skip to content

Commit c0f998d

Browse files
committed
adding requirer middleware to gin middleware
1 parent c29fd74 commit c0f998d

File tree

6 files changed

+644
-38
lines changed

6 files changed

+644
-38
lines changed

internal/jwt/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,12 @@ var ErrJWTInvalidAudience = types.ServiceError{
123123
Title: "JSON Web Token Invalid Audience",
124124
Detail: "The JSON Web Token has been issued for a different target audience",
125125
}
126+
127+
// ErrForbidden is a template error which should be used to deny requests but
128+
// allows for setting a detail message beforehand
129+
var ErrForbidden = types.ServiceError{
130+
Type: "https://www.rfc-editor.org/rfc/rfc9110#section-15.5.4",
131+
Status: 403,
132+
Title: "Access Forbidden",
133+
Detail: "You don't have the required permissions to access this resource",
134+
}

internal/jwt/requirer.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package jwt
2+
3+
type ScopeRequirer struct {
4+
Service string
5+
}
6+
7+
func (sr *ScopeRequirer) Configure(serviceName string) {
8+
sr.Service = serviceName
9+
}

middleware/gin/jwt/jwt_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package jwt
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gin-gonic/gin"
7+
"github.com/lestrrat-go/jwx/v2/jwa"
8+
"github.com/lestrrat-go/jwx/v2/jwk"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
var r *gin.Engine
13+
var jwkTestingKey = []byte("testing-key")
14+
var key jwk.Key
15+
var keySet jwk.Set
16+
var v *Validator
17+
var sr *ScopeRequirer
18+
19+
func Test(t *testing.T) {
20+
r = gin.New()
21+
22+
var err error
23+
key, err = jwk.FromRaw(jwkTestingKey)
24+
assert.NoError(t, err)
25+
26+
jwk.AssignKeyID(key)
27+
key.Set(jwk.KeyUsageKey, "sig")
28+
key.Set(jwk.AlgorithmKey, jwa.HS256)
29+
30+
keySet = jwk.NewSet()
31+
keySet.AddKey(key)
32+
33+
v = &Validator{}
34+
err = v.Configure("test", keySet, nil)
35+
assert.NoError(t, err)
36+
37+
sr = &ScopeRequirer{}
38+
sr.Configure(scopePrefix)
39+
40+
r.Use(v.Handler)
41+
r.GET("/", func(ctx *gin.Context) {
42+
ctx.Status(200)
43+
})
44+
45+
t.Run("Validator", func(t *testing.T) {
46+
t.Run("Missing_Authorization_Header", _missing_authorization_header)
47+
t.Run("Multiple_Authoritazion_Headers", _multiple_authorization_headers)
48+
t.Run("Unsupported_Token_Scheme", _unsupported_token_scheme)
49+
t.Run("Invalid_JWT", _invalid_jwt)
50+
t.Run("Missing_Claims", _missing_jwt_claims)
51+
t.Run("Invalid_Claims", _invalid_claim_values)
52+
t.Run("Empty_Scopes", _jwt_empty_scopes)
53+
t.Run("Valid_JWT", _jwt_valid)
54+
t.Run("Valid_Admin_JWT", _jwt_admin_valid)
55+
})
56+
57+
r = gin.New()
58+
r.Use(v.Handler)
59+
60+
t.Run("Requirer", func(t *testing.T) {
61+
t.Run("Read", func(t *testing.T) {
62+
63+
r.GET("/", sr.RequireRead, func(ctx *gin.Context) {
64+
ctx.Status(200)
65+
})
66+
t.Run("No_Scope", _require_read_no_scope)
67+
t.Run("Scope", _require_read)
68+
69+
t.Cleanup(func() {
70+
r = gin.New()
71+
r.Use(v.Handler)
72+
})
73+
})
74+
t.Run("Write", func(t *testing.T) {
75+
r.GET("/", sr.RequireWrite, func(ctx *gin.Context) {
76+
ctx.Status(200)
77+
})
78+
t.Run("No_Scope", _require_write_no_scope)
79+
t.Run("Scope", _require_write)
80+
t.Cleanup(func() {
81+
r = gin.New()
82+
r.Use(v.Handler)
83+
})
84+
})
85+
t.Run("Delete", func(t *testing.T) {
86+
r.GET("/", sr.RequireDelete, func(ctx *gin.Context) {
87+
ctx.Status(200)
88+
})
89+
t.Run("No_Scope", _require_delete_no_scope)
90+
t.Run("Scope", _require_delete)
91+
t.Cleanup(func() {
92+
r = gin.New()
93+
r.Use(v.Handler)
94+
})
95+
})
96+
t.Run("Admin", func(t *testing.T) {
97+
r.GET("/", sr.RequireAdministrator, func(ctx *gin.Context) {
98+
ctx.Status(200)
99+
})
100+
t.Run("No_Scope", _require_admin_no_scope)
101+
t.Run("Scope", _require_admin)
102+
t.Cleanup(func() {
103+
r = gin.New()
104+
r.Use(v.Handler)
105+
})
106+
})
107+
})
108+
}

middleware/gin/jwt/requirer.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package jwt
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"github.com/gin-gonic/gin"
8+
"github.com/wisdom-oss/common-go/v3/internal/jwt"
9+
"github.com/wisdom-oss/common-go/v3/types"
10+
)
11+
12+
type ScopeRequirer struct {
13+
jwt.ScopeRequirer
14+
}
15+
16+
func (sr *ScopeRequirer) require(scope types.Scope, c *gin.Context) {
17+
isAdministrator := c.GetBool(KeyAdministrator)
18+
if isAdministrator {
19+
c.Next()
20+
return
21+
}
22+
23+
scopes := c.GetStringSlice(KeyTokenPermissions)
24+
if len(scopes) == 0 {
25+
c.Abort()
26+
jwt.ErrForbidden.Emit(c)
27+
return
28+
29+
}
30+
31+
requiredScope := fmt.Sprintf(`%s:%s`, sr.Service, scope)
32+
33+
if !slices.Contains(scopes, requiredScope) {
34+
c.Abort()
35+
jwt.ErrForbidden.Emit(c)
36+
return
37+
}
38+
39+
}
40+
41+
func (sr *ScopeRequirer) RequireRead(c *gin.Context) {
42+
sr.require(types.ScopeRead, c)
43+
if c.IsAborted() {
44+
return
45+
}
46+
c.Next()
47+
}
48+
49+
func (sr *ScopeRequirer) RequireWrite(c *gin.Context) {
50+
sr.require(types.ScopeWrite, c)
51+
if c.IsAborted() {
52+
return
53+
}
54+
c.Next()
55+
}
56+
57+
func (sr *ScopeRequirer) RequireDelete(c *gin.Context) {
58+
sr.require(types.ScopeDelete, c)
59+
if c.IsAborted() {
60+
return
61+
}
62+
c.Next()
63+
}
64+
65+
func (sr *ScopeRequirer) RequireAdministrator(c *gin.Context) {
66+
isAdministrator := c.GetBool(KeyAdministrator)
67+
if isAdministrator {
68+
c.Next()
69+
return
70+
}
71+
72+
c.Abort()
73+
jwt.ErrForbidden.Emit(c)
74+
}

0 commit comments

Comments
 (0)