Skip to content

Commit 1c91aa0

Browse files
authored
feat(http): Create jwt middlewares for http servers (chainloop-dev#1099)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent 05309bc commit 1c91aa0

File tree

3 files changed

+353
-29
lines changed

3 files changed

+353
-29
lines changed

app/artifact-cas/internal/server/http.go

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,17 @@ import (
1919
"fmt"
2020
"os"
2121

22+
api "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1"
2223
"github.com/chainloop-dev/chainloop/app/artifact-cas/internal/conf"
2324
"github.com/chainloop-dev/chainloop/app/artifact-cas/internal/service"
2425
casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
25-
jwt "github.com/golang-jwt/jwt/v4"
26-
27-
nhttp "net/http"
28-
29-
api "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1"
3026
backend "github.com/chainloop-dev/chainloop/pkg/blobmanager"
27+
middlewares_http "github.com/chainloop-dev/chainloop/pkg/middlewares/http"
3128
"github.com/go-kratos/kratos/v2/log"
32-
jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
3329
"github.com/go-kratos/kratos/v2/middleware/logging"
3430
"github.com/go-kratos/kratos/v2/middleware/recovery"
3531
"github.com/go-kratos/kratos/v2/transport/http"
32+
"github.com/golang-jwt/jwt/v4"
3633
)
3734

3835
// NewHTTPServer new a HTTP server.
@@ -68,31 +65,14 @@ func NewHTTPServer(c *conf.Server, authConf *conf.Auth, downloadSvc *service.Dow
6865

6966
srv := http.NewServer(opts...)
7067

71-
srv.Handle(service.DownloadPath, authFromQueryMiddleware(loadPublicKey(rawKey), casJWT.SigningMethod, downloadSvc))
68+
srv.Handle(service.DownloadPath, middlewares_http.AuthFromQueryParam(loadPublicKey(rawKey), claimsFunc(), casJWT.SigningMethod, downloadSvc))
7269
api.RegisterStatusServiceHTTPServer(srv, service.NewStatusService(Version, providers))
7370
return srv, nil
7471
}
7572

76-
func authFromQueryMiddleware(keyFunc jwt.Keyfunc, signingMethod jwt.SigningMethod, next nhttp.Handler) nhttp.Handler {
77-
return nhttp.HandlerFunc(func(w http.ResponseWriter, r *nhttp.Request) {
78-
token := r.URL.Query().Get("t")
79-
if token == "" {
80-
nhttp.Error(w, "missing token", nhttp.StatusUnauthorized)
81-
return
82-
}
83-
84-
claims, err := verifyAndMarshalJWT(token, keyFunc, signingMethod)
85-
if err != nil {
86-
// return unauthorized
87-
nhttp.Error(w, "invalid token", nhttp.StatusUnauthorized)
88-
return
89-
}
90-
91-
// Attach the claims to the context
92-
ctx := jwtMiddleware.NewContext(r.Context(), claims)
93-
r = r.WithContext(ctx)
94-
95-
// Run the next handler
96-
next.ServeHTTP(w, r)
97-
})
73+
// claimsFunc returns the claims function for the JWT middleware that casts the claims to the correct type
74+
func claimsFunc() middlewares_http.ClaimsFunc {
75+
return func() jwt.Claims {
76+
return &casJWT.Claims{}
77+
}
9878
}

pkg/middlewares/http/jwt.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package middlewareshttp
17+
18+
import (
19+
nhttp "net/http"
20+
"strings"
21+
22+
"github.com/go-kratos/kratos/v2/errors"
23+
jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
24+
"github.com/go-kratos/kratos/v2/transport/http"
25+
"github.com/golang-jwt/jwt/v4"
26+
)
27+
28+
const (
29+
// bearerWord the bearer key word for authorization
30+
bearerWord string = "Bearer"
31+
// authorizationKey holds the key used to store the JWT Token in the request tokenHeader.
32+
authorizationKey string = "Authorization"
33+
)
34+
35+
// ClaimsFunc is a function that returns a jwt.Claims with the custom claims and correct type
36+
type ClaimsFunc func() jwt.Claims
37+
38+
// AuthFromQueryParam is a middleware that extracts the token from the query parameter and verifies it
39+
func AuthFromQueryParam(keyFunc jwt.Keyfunc, claimsFunc ClaimsFunc, signingMethod jwt.SigningMethod, next nhttp.Handler) nhttp.Handler {
40+
return nhttp.HandlerFunc(func(w http.ResponseWriter, r *nhttp.Request) {
41+
token := r.URL.Query().Get("t")
42+
if token == "" {
43+
nhttp.Error(w, "missing token", nhttp.StatusUnauthorized)
44+
return
45+
}
46+
47+
verifyJWTAndServeNext(w, r, token, keyFunc, claimsFunc, signingMethod, next)
48+
})
49+
}
50+
51+
// verifyJWTAndServeNext verifies the token and serves the next handler
52+
func verifyJWTAndServeNext(w http.ResponseWriter, r *nhttp.Request, token string, keyFunc jwt.Keyfunc, claimsFunc ClaimsFunc, signingMethod jwt.SigningMethod, next nhttp.Handler) {
53+
claims, err := verifyAndMarshalJWT(token, keyFunc, claimsFunc, signingMethod)
54+
if err != nil {
55+
// return unauthorized
56+
nhttp.Error(w, "invalid token", nhttp.StatusUnauthorized)
57+
return
58+
}
59+
60+
// Attach the claims to the context
61+
ctx := jwtMiddleware.NewContext(r.Context(), *claims)
62+
r = r.WithContext(ctx)
63+
64+
// Run the next handler
65+
next.ServeHTTP(w, r)
66+
}
67+
68+
// AuthFromAuthorizationHeader is a middleware that extracts the token from the authorization header and verifies it
69+
func AuthFromAuthorizationHeader(keyFunc jwt.Keyfunc, claimsFunc ClaimsFunc, signingMethod jwt.SigningMethod, next nhttp.Handler) nhttp.Handler {
70+
return nhttp.HandlerFunc(func(w http.ResponseWriter, r *nhttp.Request) {
71+
auths := strings.SplitN(r.Header.Get(authorizationKey), " ", 2)
72+
if len(auths) != 2 || !strings.EqualFold(auths[0], bearerWord) {
73+
nhttp.Error(w, "JWT token is missing", nhttp.StatusUnauthorized)
74+
return
75+
}
76+
77+
jwtToken := auths[1]
78+
79+
verifyJWTAndServeNext(w, r, jwtToken, keyFunc, claimsFunc, signingMethod, next)
80+
})
81+
}
82+
83+
// verifyAndMarshalJWT verifies the token and returns the map claims
84+
func verifyAndMarshalJWT(token string, keyFunc jwt.Keyfunc, claimsFunc ClaimsFunc, signingMethod jwt.SigningMethod) (*jwt.Claims, error) {
85+
var tokenInfo *jwt.Token
86+
87+
tokenInfo, err := jwt.ParseWithClaims(token, claimsFunc(), keyFunc)
88+
if err != nil {
89+
var ve *jwt.ValidationError
90+
if !errors.As(err, &ve) {
91+
return nil, errors.Unauthorized("UNAUTHORIZED", err.Error())
92+
}
93+
94+
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
95+
return nil, jwtMiddleware.ErrTokenInvalid
96+
}
97+
98+
if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
99+
return nil, jwtMiddleware.ErrTokenExpired
100+
}
101+
102+
if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
103+
return nil, jwtMiddleware.ErrTokenExpired
104+
}
105+
106+
return nil, err
107+
}
108+
109+
if !tokenInfo.Valid {
110+
return nil, jwtMiddleware.ErrTokenInvalid
111+
}
112+
113+
if tokenInfo.Method != signingMethod {
114+
return nil, jwtMiddleware.ErrUnSupportSigningMethod
115+
}
116+
117+
return &tokenInfo.Claims, nil
118+
}

0 commit comments

Comments
 (0)