git.iko.soy/typeable/oauth2-proxy
on master help sign in
Integrate sessions middlewares
commiteb234011eb5135aa699072e07acfd43d63375a92 authorJoel Speed <joel.speed@hotmail.co.uk> dateSat 18 Jul 2020, 00:42 (6 years ago) parent034f057
4 files changed+53−309
oauthproxy.go+49−173 oauthproxy_test.go+1−65 providers/provider_default.go+3−30 providers/provider_default_test.go−41
oauthproxy.go+49−173
@@ -15,7 +15,9 @@ import (
1515 "time"
1616
1717 "github.com/coreos/go-oidc"
18+ "github.com/justinas/alice"
1819 ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
20+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
1921 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
2022 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
2123 "github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
@@ -23,6 +25,7 @@ import (
2325 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
2426 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
2527 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
28+ "github.com/oauth2-proxy/oauth2-proxy/pkg/middleware"
2629 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
2730 "github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
2831 "github.com/oauth2-proxy/oauth2-proxy/providers"
@@ -98,6 +101,8 @@ type OAuthProxy struct {
98101 trustedIPs *ip.NetSet
99102 Banner string
100103 Footer string
104+
105+ sessionChain alice.Chain
101106}
102107
103108// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
@@ -156,6 +161,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
156161 }
157162 }
158163
164+ sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
165+
159166 return &OAuthProxy{
160167 CookieName: opts.Cookie.Name,
161168 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
@@ -209,9 +216,45 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
209216
210217 basicAuthValidator: basicAuthValidator,
211218 displayHtpasswdForm: basicAuthValidator != nil,
219+ sessionChain: sessionChain,
212220 }, nil
213221}
214222
223+func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
224+ chain := alice.New(middleware.NewScope())
225+
226+ if opts.SkipJwtBearerTokens {
227+ sessionLoaders := []middlewareapi.TokenToSessionLoader{}
228+ if opts.GetOIDCVerifier() != nil {
229+ sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
230+ Verifier: opts.GetOIDCVerifier(),
231+ TokenToSession: opts.GetProvider().CreateSessionStateFromBearerToken,
232+ })
233+ }
234+
235+ for _, verifier := range opts.GetJWTBearerVerifiers() {
236+ sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
237+ Verifier: verifier,
238+ })
239+ }
240+
241+ chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
242+ }
243+
244+ if validator != nil {
245+ chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
246+ }
247+
248+ chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
249+ SessionStore: sessionStore,
250+ RefreshPeriod: opts.Cookie.Refresh,
251+ RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
252+ ValidateSessionState: opts.GetProvider().ValidateSessionState,
253+ }))
254+
255+ return chain
256+}
257+
215258// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
216259// redirect clients to once authenticated
217260func (p *OAuthProxy) GetRedirectURI(host string) string {
@@ -780,86 +823,20 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
780823// Set-Cookie headers may be set on the response as a side-effect of calling this method.
781824func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
782825 var session *sessionsapi.SessionState
783− var err error
784− var saveSession, clearSession, revalidated bool
785826
786− if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
787− session, err = p.GetJwtSession(req)
788− if err != nil {
789− logger.Printf("Error retrieving session from token in Authorization header: %s", err)
790− }
791− if session != nil {
792− saveSession = false
793− }
794− }
827+ getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
828+ session = middleware.GetRequestScope(req).Session
829+ }))
830+ getSession.ServeHTTP(rw, req)
795831
796− remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
797832 if session == nil {
798− session, err = p.LoadCookiedSession(req)
799− if err != nil {
800− logger.Printf("Error loading cookied session: %s", err)
801− }
802−
803− if session != nil {
804− if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
805− logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
806− saveSession = true
807− }
808−
809− if ok, err := p.provider.RefreshSessionIfNeeded(req.Context(), session); err != nil {
810− logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
811− clearSession = true
812− session = nil
813− } else if ok {
814− saveSession = true
815− revalidated = true
816− }
817− }
818− }
819−
820− if session != nil && session.IsExpired() {
821− logger.Printf("Removing session: token expired %s", session)
822− session = nil
823− saveSession = false
824− clearSession = true
825− }
826−
827− if saveSession && !revalidated && session != nil && session.AccessToken != "" {
828− if !p.provider.ValidateSessionState(req.Context(), session) {
829− logger.Printf("Removing session: error validating %s", session)
830− saveSession = false
831− session = nil
832− clearSession = true
833− }
833+ return nil, ErrNeedsLogin
834834 }
835835
836836 if session != nil && session.Email != "" && !p.Validator(session.Email) {
837837 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
838− session = nil
839− saveSession = false
840− clearSession = true
841− }
842−
843− if saveSession && session != nil {
844− err = p.SaveSession(rw, req, session)
845− if err != nil {
846− logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
847− return nil, err
848− }
849− }
850−
851− if clearSession {
838+ // Invalid session, clear it
852839 p.ClearSessionCookie(rw, req)
853− }
854−
855− if session == nil {
856− session, err = p.CheckBasicAuth(req)
857− if err != nil {
858− logger.Printf("Error during basic auth validation: %s", err)
859− }
860− }
861−
862− if session == nil {
863840 return nil, ErrNeedsLogin
864841 }
865842
@@ -997,36 +974,6 @@ func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
997974 }
998975}
999976
1000−// CheckBasicAuth checks the requests Authorization header for basic auth
1001−// credentials and authenticates these against the proxies HtpasswdFile
1002−func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
1003− if p.basicAuthValidator == nil {
1004− return nil, nil
1005− }
1006− auth := req.Header.Get("Authorization")
1007− if auth == "" {
1008− return nil, nil
1009− }
1010− s := strings.SplitN(auth, " ", 2)
1011− if len(s) != 2 || s[0] != "Basic" {
1012− return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
1013− }
1014− b, err := b64.StdEncoding.DecodeString(s[1])
1015− if err != nil {
1016− return nil, err
1017− }
1018− pair := strings.SplitN(string(b), ":", 2)
1019− if len(pair) != 2 {
1020− return nil, fmt.Errorf("invalid format %s", b)
1021− }
1022− if p.basicAuthValidator.Validate(pair[0], pair[1]) {
1023− logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
1024− return &sessionsapi.SessionState{User: pair[0]}, nil
1025− }
1026− logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
1027− return nil, nil
1028−}
1029−
1030977// isAjax checks if a request is an ajax request
1031978func isAjax(req *http.Request) bool {
1032979 acceptValues := req.Header.Values("Accept")
@@ -1044,74 +991,3 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
1044991 rw.Header().Set("Content-Type", applicationJSON)
1045992 rw.WriteHeader(code)
1046993}
1047−
1048−// GetJwtSession loads a session based on a JWT token in the authorization header.
1049−// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
1050−func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
1051− rawBearerToken, err := p.findBearerToken(req)
1052− if err != nil {
1053− return nil, err
1054− }
1055−
1056− // If we are using an oidc provider, go ahead and try that provider first with its Verifier
1057− // and Bearer Token -> Session converter
1058− if p.mainJwtBearerVerifier != nil {
1059− bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
1060− if err == nil {
1061− return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1062− }
1063− }
1064−
1065− // Otherwise, attempt to verify against the extra JWT issuers and use a more generic
1066− // Bearer Token -> Session converter
1067− for _, verifier := range p.extraJwtBearerVerifiers {
1068− bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
1069− if err != nil {
1070− continue
1071− }
1072−
1073− return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
1074− }
1075− return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
1076−}
1077−
1078−// findBearerToken finds a valid JWT token from the Authorization header of a given request.
1079−func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
1080− auth := req.Header.Get("Authorization")
1081− s := strings.SplitN(auth, " ", 2)
1082− if len(s) != 2 {
1083− return "", fmt.Errorf("invalid authorization header %s", auth)
1084− }
1085− jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
1086− var rawBearerToken string
1087− if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
1088− rawBearerToken = s[1]
1089− } else if s[0] == "Basic" {
1090− // Check if we have a Bearer token masquerading in Basic
1091− b, err := b64.StdEncoding.DecodeString(s[1])
1092− if err != nil {
1093− return "", err
1094− }
1095− pair := strings.SplitN(string(b), ":", 2)
1096− if len(pair) != 2 {
1097− return "", fmt.Errorf("invalid format %s", b)
1098− }
1099− user, password := pair[0], pair[1]
1100−
1101− // check user, user+password, or just password for a token
1102− if jwtRegex.MatchString(user) {
1103− // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
1104− if password == "" || password == "x-oauth-basic" {
1105− rawBearerToken = user
1106− }
1107− } else if jwtRegex.MatchString(password) {
1108− // support passwords and ignore user
1109− rawBearerToken = password
1110− }
1111− }
1112− if rawBearerToken == "" {
1113− return "", fmt.Errorf("no valid bearer token found in authorization header")
1114− }
1115−
1116− return rawBearerToken, nil
1117−}
oauthproxy_test.go+1−65
@@ -1889,7 +1889,7 @@ func TestGetJwtSession(t *testing.T) {
18891889
18901890 // Bearer
18911891 expires := time.Unix(1912151821, 0)
1892− session, err := test.proxy.GetJwtSession(test.req)
1892+ session, err := test.proxy.getAuthenticatedSession(test.rw, test.req)
18931893 assert.NoError(t, err)
18941894 assert.Equal(t, session.User, "1234567890")
18951895 assert.Equal(t, session.Email, "john@example.com")
@@ -1912,70 +1912,6 @@ func TestGetJwtSession(t *testing.T) {
19121912 assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
19131913}
19141914
1915−func TestFindJwtBearerToken(t *testing.T) {
1916− p := OAuthProxy{CookieName: "oauth2", CookieDomains: []string{"abc"}}
1917− getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}}
1918−
1919− validToken := "eyJfoobar.eyJfoobar.12345asdf"
1920− var token string
1921−
1922− // Bearer
1923− getReq.Header = map[string][]string{
1924− "Authorization": {fmt.Sprintf("Bearer %s", validToken)},
1925− }
1926−
1927− token, err := p.findBearerToken(getReq)
1928− assert.NoError(t, err)
1929− assert.Equal(t, validToken, token)
1930−
1931− // Basic - no password
1932− getReq.SetBasicAuth(token, "")
1933− token, err = p.findBearerToken(getReq)
1934− assert.NoError(t, err)
1935− assert.Equal(t, validToken, token)
1936−
1937− // Basic - sentinel password
1938− getReq.SetBasicAuth(token, "x-oauth-basic")
1939− token, err = p.findBearerToken(getReq)
1940− assert.NoError(t, err)
1941− assert.Equal(t, validToken, token)
1942−
1943− // Basic - any username, password matching jwt pattern
1944− getReq.SetBasicAuth("any-username-you-could-wish-for", token)
1945− token, err = p.findBearerToken(getReq)
1946− assert.NoError(t, err)
1947− assert.Equal(t, validToken, token)
1948−
1949− failures := []string{
1950− // Too many parts
1951− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1952− // Not enough parts
1953− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA",
1954− // Invalid encrypted key
1955− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.//////.dGVzdA.dGVzdA.dGVzdA",
1956− // Invalid IV
1957− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.//////.dGVzdA.dGVzdA",
1958− // Invalid ciphertext
1959− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.//////.dGVzdA",
1960− // Invalid tag
1961− "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.//////",
1962− // Invalid header
1963− "W10.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1964− // Invalid header
1965− "######.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1966− // Missing alc/enc params
1967− "e30.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1968− }
1969−
1970− for _, failure := range failures {
1971− getReq.Header = map[string][]string{
1972− "Authorization": {fmt.Sprintf("Bearer %s", failure)},
1973− }
1974− _, err := p.findBearerToken(getReq)
1975− assert.Error(t, err)
1976− }
1977−}
1978−
19791915func Test_prepareNoCache(t *testing.T) {
19801916 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19811917 prepareNoCache(w)
providers/provider_default.go+3−30
@@ -126,35 +126,8 @@ func (p *ProviderData) RefreshSessionIfNeeded(ctx context.Context, s *sessions.S
126126 return false, nil
127127}
128128
129+// CreateSessionStateFromBearerToken should be implemented to allow providers
130+// to convert ID tokens into sessions
129131func (p *ProviderData) CreateSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error) {
130− var claims struct {
131− Subject string `json:"sub"`
132− Email string `json:"email"`
133− Verified *bool `json:"email_verified"`
134− PreferredUsername string `json:"preferred_username"`
135− }
136−
137− if err := idToken.Claims(&claims); err != nil {
138− return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
139− }
140−
141− if claims.Email == "" {
142− claims.Email = claims.Subject
143− }
144−
145− if claims.Verified != nil && !*claims.Verified {
146− return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
147− }
148−
149− newSession := &sessions.SessionState{
150− Email: claims.Email,
151− User: claims.Subject,
152− PreferredUsername: claims.PreferredUsername,
153− AccessToken: rawIDToken,
154− IDToken: rawIDToken,
155− RefreshToken: "",
156− ExpiresOn: &idToken.Expiry,
157− }
158−
159− return newSession, nil
132+ return nil, errors.New("not implemented")
160133}
providers/provider_default_test.go−41
@@ -2,15 +2,10 @@ package providers
22
33import (
44 "context"
5− "crypto/rand"
6− "crypto/rsa"
75 "net/url"
86 "testing"
97 "time"
108
11− "github.com/coreos/go-oidc"
12− "github.com/dgrijalva/jwt-go"
13−
149 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
1510 "github.com/stretchr/testify/assert"
1611)
@@ -52,39 +47,3 @@ func TestAcrValuesConfigured(t *testing.T) {
5247 result := p.GetLoginURL("https://my.test.app/oauth", "")
5348 assert.Contains(t, result, "acr_values=testValue")
5449}
55−
56−func TestCreateSessionStateFromBearerToken(t *testing.T) {
57− minimalIDToken := jwt.StandardClaims{
58− Audience: "asdf1234",
59− ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
60− Id: "id-some-id",
61− IssuedAt: time.Now().Unix(),
62− Issuer: "https://issuer.example.com",
63− NotBefore: 0,
64− Subject: "123456789",
65− }
66− // From oidc_test.go
67− verifier := oidc.NewVerifier(
68− "https://issuer.example.com",
69− fakeKeySetStub{},
70− &oidc.Config{ClientID: "asdf1234"},
71− )
72−
73− key, err := rsa.GenerateKey(rand.Reader, 2048)
74− assert.NoError(t, err)
75− rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, minimalIDToken).SignedString(key)
76− assert.NoError(t, err)
77− // Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below
78− idToken, err := verifier.Verify(context.Background(), rawIDToken)
79− assert.NoError(t, err)
80−
81− session, err := (*ProviderData)(nil).CreateSessionStateFromBearerToken(context.Background(), rawIDToken, idToken)
82− assert.NoError(t, err)
83−
84− assert.Equal(t, rawIDToken, session.AccessToken)
85− assert.Equal(t, rawIDToken, session.IDToken)
86− assert.Equal(t, "123456789", session.Email)
87− assert.Equal(t, "123456789", session.User)
88− assert.Empty(t, session.RefreshToken)
89− assert.Empty(t, session.PreferredUsername)
90−}