git.iko.soy/typeable/oauth2-proxy
on remove-gap-headers help sign in
169 lines 5.4 kB history raw
1package middleware
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "regexp"
9 "github.com/coreos/go-oidc"
10 "github.com/justinas/alice"
11 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
12 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
16const jwtRegexFormat = `^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`
18func NewJwtSessionLoader(sessionLoaders []middlewareapi.TokenToSessionLoader) alice.Constructor {
19 for i, loader := range sessionLoaders {
20 if loader.TokenToSession == nil {
21 sessionLoaders[i] = middlewareapi.TokenToSessionLoader{
22 Verifier: loader.Verifier,
23 TokenToSession: createSessionStateFromBearerToken,
24 }
25 }
26 }
28 js := &jwtSessionLoader{
29 jwtRegex: regexp.MustCompile(jwtRegexFormat),
30 sessionLoaders: sessionLoaders,
31 }
32 return js.loadSession
35// jwtSessionLoader is responsible for loading sessions from JWTs in
36// Authorization headers.
37type jwtSessionLoader struct {
38 jwtRegex *regexp.Regexp
39 sessionLoaders []middlewareapi.TokenToSessionLoader
42// loadSession attempts to load a session from a JWT stored in an Authorization
43// header within the request.
44// If no authorization header is found, or the header is invalid, no session
45// will be loaded and the request will be passed to the next handler.
46// If a session was loaded by a previous handler, it will not be replaced.
47func (j *jwtSessionLoader) loadSession(next http.Handler) http.Handler {
48 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
49 scope := GetRequestScope(req)
50 // If scope is nil, this will panic.
51 // A scope should always be injected before this handler is called.
52 if scope.Session != nil {
53 // The session was already loaded, pass to the next handler
54 next.ServeHTTP(rw, req)
55 return
56 }
58 session, err := j.getJwtSession(req)
59 if err != nil {
60 logger.Errorf("Error retrieving session from token in Authorization header: %v", err)
61 }
63 // Add the session to the scope if it was found
64 scope.Session = session
65 next.ServeHTTP(rw, req)
66 })
69// getJwtSession loads a session based on a JWT token in the authorization header.
70// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
71func (j *jwtSessionLoader) getJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
72 auth := req.Header.Get("Authorization")
73 if auth == "" {
74 // No auth header provided, so don't attempt to load a session
75 return nil, nil
76 }
78 rawBearerToken, err := j.findBearerTokenFromHeader(auth)
79 if err != nil {
80 return nil, err
81 }
83 for _, loader := range j.sessionLoaders {
84 bearerToken, err := loader.Verifier.Verify(req.Context(), rawBearerToken)
85 if err == nil {
86 // The token was verified, convert it to a session
87 return loader.TokenToSession(req.Context(), rawBearerToken, bearerToken)
88 }
89 }
91 return nil, fmt.Errorf("unable to verify jwt token: %q", req.Header.Get("Authorization"))
94// findBearerTokenFromHeader finds a valid JWT token from the Authorization header of a given request.
95func (j *jwtSessionLoader) findBearerTokenFromHeader(header string) (string, error) {
96 tokenType, token, err := splitAuthHeader(header)
97 if err != nil {
98 return "", err
99 }
101 if tokenType == "Bearer" && j.jwtRegex.MatchString(token) {
102 // Found a JWT as a bearer token
103 return token, nil
104 }
106 if tokenType == "Basic" {
107 // Check if we have a Bearer token masquerading in Basic
108 return j.getBasicToken(token)
109 }
111 return "", fmt.Errorf("no valid bearer token found in authorization header")
114// getBasicToken tries to extract a token from the basic value provided.
115func (j *jwtSessionLoader) getBasicToken(token string) (string, error) {
116 user, password, err := getBasicAuthCredentials(token)
117 if err != nil {
118 return "", err
119 }
121 // check user, user+password, or just password for a token
122 if j.jwtRegex.MatchString(user) {
123 // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
124 /* #nosec G101 */
125 if password == "" || password == "x-oauth-basic" {
126 return user, nil
127 }
128 } else if j.jwtRegex.MatchString(password) {
129 // support passwords and ignore user
130 return password, nil
131 }
133 return "", fmt.Errorf("invalid basic auth token found in authorization header")
136// createSessionStateFromBearerToken is a default implementation for converting
137// a JWT into a session state.
138func createSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessionsapi.SessionState, error) {
139 var claims struct {
140 Subject string `json:"sub"`
141 Email string `json:"email"`
142 Verified *bool `json:"email_verified"`
143 PreferredUsername string `json:"preferred_username"`
144 }
146 if err := idToken.Claims(&claims); err != nil {
147 return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
148 }
150 if claims.Email == "" {
151 claims.Email = claims.Subject
152 }
154 if claims.Verified != nil && !*claims.Verified {
155 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
156 }
158 newSession := &sessionsapi.SessionState{
159 Email: claims.Email,
160 User: claims.Subject,
161 PreferredUsername: claims.PreferredUsername,
162 AccessToken: rawIDToken,
163 IDToken: rawIDToken,
164 RefreshToken: "",
165 ExpiresOn: &idToken.Expiry,
166 }
168 return newSession, nil