• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

m-lab / autojoin / 16029916907

02 Jul 2025 03:54PM UTC coverage: 89.842% (-2.8%) from 92.629%
16029916907

Pull #73

github

robertodauria
Update prometheus label
Pull Request #73: Support JWT authentication

4 of 46 new or added lines in 1 file covered. (8.7%)

2 existing lines in 1 file now uncovered.

1309 of 1457 relevant lines covered (89.84%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.11
/handler/validator.go
1
package handler
2

3
import (
4
        "context"
5
        "errors"
6
        "net/http"
7
        "strings"
8

9
        "github.com/golang-jwt/jwt/v4"
10
        v0 "github.com/m-lab/autojoin/api/v0"
11
        v2 "github.com/m-lab/locate/api/v2"
12
)
13

14
type contextKey string
15

16
const orgContextKey contextKey = "organization"
17

18
// APIKeyValidator is an interface for validating API keys and retrieving
19
// associated organization info.
20
type APIKeyValidator interface {
21
        // ValidateKey validates the provided API key and returns the associated
22
        // organization name if valid. Returns an error if the key is invalid.
23
        ValidateKey(ctx context.Context, key string) (string, error)
24
}
25

26
// WithAPIKeyValidation creates middleware that validates API keys and adds
27
// org info to context.
28
func WithAPIKeyValidation(validator APIKeyValidator, next http.HandlerFunc) http.HandlerFunc {
1✔
29
        return func(w http.ResponseWriter, r *http.Request) {
2✔
30
                // Use the API key from the query string, extract the organization
1✔
31
                // from Datastore.
1✔
32
                apiKey := r.URL.Query().Get("api_key")
1✔
33
                if apiKey == "" {
2✔
34
                        resp := v0.RegisterResponse{
1✔
35
                                Error: &v2.Error{
1✔
36
                                        Type:   "?api_key=<key>",
1✔
37
                                        Title:  "API key is required",
1✔
38
                                        Status: http.StatusUnauthorized,
1✔
39
                                },
1✔
40
                        }
1✔
41
                        w.WriteHeader(resp.Error.Status)
1✔
42
                        writeResponse(w, resp)
1✔
43
                        return
1✔
44
                }
1✔
45

46
                org, err := validator.ValidateKey(r.Context(), apiKey)
1✔
47
                if err != nil {
2✔
48
                        resp := v0.RegisterResponse{
1✔
49
                                Error: &v2.Error{
1✔
50
                                        Type:   "auth.invalid_key",
1✔
51
                                        Title:  "Invalid API key",
1✔
52
                                        Status: http.StatusUnauthorized,
1✔
53
                                },
1✔
54
                        }
1✔
55
                        w.WriteHeader(resp.Error.Status)
1✔
56
                        writeResponse(w, resp)
1✔
57
                        return
1✔
58
                }
1✔
59

60
                ctx := context.WithValue(r.Context(), orgContextKey, org)
1✔
61
                next.ServeHTTP(w, r.WithContext(ctx))
1✔
62
        }
63
}
64

65
// validateJWTAndExtractOrg validates the JWT and extracts the "org" claim.
NEW
66
func validateJWTAndExtractOrg(tokenString string) (string, error) {
×
NEW
67
        // Note: This JWT *must* be verified previously in the stack, e.g. via openapi
×
NEW
68
        // security definitions.
×
NEW
69
        token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
×
NEW
70
        if err != nil {
×
NEW
71
                return "", err
×
NEW
72
        }
×
NEW
73
        if claims, ok := token.Claims.(jwt.MapClaims); ok {
×
NEW
74
                if org, ok := claims["org"].(string); ok {
×
NEW
75
                        return org, nil
×
NEW
76
                }
×
77
        }
NEW
78
        return "", errors.New("org claim not found")
×
79
}
80

81
// WithJWTValidation creates middleware that validates JWT tokens only and adds
82
// org info to context. This is used for the /register-jwt endpoint.
NEW
83
func WithJWTValidation(next http.HandlerFunc) http.HandlerFunc {
×
NEW
84
        return func(w http.ResponseWriter, r *http.Request) {
×
NEW
85
                // Check for the Authorization header.
×
NEW
86
                authHeader := r.Header.Get("Authorization")
×
NEW
87
                if !strings.HasPrefix(authHeader, "Bearer ") {
×
NEW
88
                        resp := v0.RegisterResponse{
×
NEW
89
                                Error: &v2.Error{
×
NEW
90
                                        Type:   "auth.missing_token",
×
NEW
91
                                        Title:  "JWT token is required in Authorization header",
×
NEW
92
                                        Status: http.StatusUnauthorized,
×
NEW
93
                                },
×
NEW
94
                        }
×
NEW
95
                        w.WriteHeader(resp.Error.Status)
×
NEW
96
                        writeResponse(w, resp)
×
NEW
97
                        return
×
NEW
98
                }
×
99

NEW
100
                tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
×
NEW
101
                org, err := validateJWTAndExtractOrg(tokenString)
×
NEW
102
                if err != nil || org == "" {
×
NEW
103
                        resp := v0.RegisterResponse{
×
NEW
104
                                Error: &v2.Error{
×
NEW
105
                                        Type:   "auth.invalid_token",
×
NEW
106
                                        Title:  "Invalid or missing org claim in JWT",
×
NEW
107
                                        Status: http.StatusUnauthorized,
×
NEW
108
                                },
×
NEW
109
                        }
×
NEW
110
                        w.WriteHeader(resp.Error.Status)
×
NEW
111
                        writeResponse(w, resp)
×
NEW
112
                        return
×
NEW
113
                }
×
114

UNCOV
115
                ctx := context.WithValue(r.Context(), orgContextKey, org)
×
UNCOV
116
                next.ServeHTTP(w, r.WithContext(ctx))
×
117
        }
118
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc