-
Notifications
You must be signed in to change notification settings - Fork 326
/
auth.go
180 lines (146 loc) · 5.95 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/*
Copyright 2020 The Vouch Proxy Authors.
Use of this source code is governed by The MIT License (MIT) that
can be found in the LICENSE file. Software distributed under The
MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
OR CONDITIONS OF ANY KIND, either express or implied.
*/
package handlers
import (
"fmt"
"net/http"
"github.com/vouch/vouch-proxy/pkg/cfg"
"github.com/vouch/vouch-proxy/pkg/cookie"
"github.com/vouch/vouch-proxy/pkg/domains"
"github.com/vouch/vouch-proxy/pkg/jwtmanager"
"github.com/vouch/vouch-proxy/pkg/responses"
"github.com/vouch/vouch-proxy/pkg/structs"
"golang.org/x/oauth2"
)
// CallbackHandler /auth
// - redirects to /auth/{state}/ with the state coming from the query parameter
func CallbackHandler(w http.ResponseWriter, r *http.Request) {
log.Debug("/auth")
// did the IdP return an error?
errorIDP := r.URL.Query().Get("error")
if errorIDP != "" {
errorDescription := r.URL.Query().Get("error_description")
responses.Error401HTTP(w, r, fmt.Errorf("/auth Error from IdP: %s - %s", errorIDP, errorDescription))
return
}
queryState := r.URL.Query().Get("state")
if queryState == "" {
responses.Error400(w, r, fmt.Errorf("/auth: could not find state in query %s", r.URL.RawQuery))
return
}
// has to have a trailing / in its path, because the path of the session cookie is set to /auth/{state}/.
// see note in login.go and https://github.com/vouch/vouch-proxy/issues/373
authStateURL := fmt.Sprintf("%s/auth/%s/?%s", cfg.Cfg.DocumentRoot, queryState, r.URL.RawQuery)
responses.Redirect302(w, r, authStateURL)
}
// AuthStateHandler /auth/{state}/
// - validate info from oauth provider (Google, GitHub, OIDC, etc)
// - issue jwt in the form of a cookie
func AuthStateHandler(w http.ResponseWriter, r *http.Request) {
log.Debug("/auth/{state}/")
// Handle the exchange code to initiate a transport.
session, err := sessstore.Get(r, cfg.Cfg.Session.Name)
if err != nil {
responses.Error400(w, r, fmt.Errorf("/auth %w: could not find session store %s", err, cfg.Cfg.Session.Name))
return
}
// is the nonce "state" valid?
queryState := r.URL.Query().Get("state")
if session.Values["state"] != queryState {
responses.Error400(w, r, fmt.Errorf("/auth Invalid session state: stored %s, returned %s", session.Values["state"], queryState))
return
}
user := structs.User{}
customClaims := structs.CustomClaims{}
ptokens := structs.PTokens{}
// is code challenge enabled?
authCodeOptions := []oauth2.AuthCodeOption{}
if cfg.GenOAuth.CodeChallengeMethod != "" {
authCodeOptions = []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("code_challenge", session.Values["codeChallenge"].(string)),
oauth2.SetAuthURLParam("code_verifier", session.Values["codeVerifier"].(string)),
}
}
if err := getUserInfo(r, &user, &customClaims, &ptokens, authCodeOptions...); err != nil {
responses.Error400(w, r, fmt.Errorf("/auth Error while retrieving user info after successful login at the OAuth provider: %w", err))
return
}
log.Debugf("/auth/{state}/ Claims from userinfo: %+v", customClaims)
// verify / authz the user
if ok, err := verifyUser(user); !ok {
responses.Error403(w, r, fmt.Errorf("/auth User is not authorized: %w . Please try again or seek support from your administrator", err))
return
}
// SUCCESS!! they are authorized
// issue the jwt
tokenstring, err := jwtmanager.NewVPJWT(user, customClaims, ptokens)
if err != nil {
responses.Error500(w, r, fmt.Errorf("/auth Token creation failure: %w . Please seek support from your administrator", err))
return
}
cookie.SetCookie(w, r, tokenstring)
// get the originally requested URL so we can send them on their way
requestedURL := session.Values["requestedURL"].(string)
if requestedURL != "" {
// clear out the session value
session.Values["requestedURL"] = ""
session.Values[requestedURL] = 0
session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil {
log.Error(err)
}
responses.Redirect302(w, r, requestedURL)
return
}
// otherwise serve an error
responses.RenderIndex(w, "/auth "+tokenstring)
}
// verifyUser validates that the domains match for the user
func verifyUser(u interface{}) (bool, error) {
user := u.(structs.User)
switch {
// AllowAllUsers
case cfg.Cfg.AllowAllUsers:
log.Debugf("verifyUser: Success! skipping verification, cfg.Cfg.AllowAllUsers is %t", cfg.Cfg.AllowAllUsers)
return true, nil
// WhiteList
case len(cfg.Cfg.WhiteList) != 0:
for _, wl := range cfg.Cfg.WhiteList {
if user.Username == wl {
log.Debugf("verifyUser: Success! found user.Username in WhiteList: %s", user.Username)
return true, nil
}
}
return false, fmt.Errorf("verifyUser: user.Username not found in WhiteList: %s", user.Username)
// TeamWhiteList
case len(cfg.Cfg.TeamWhiteList) != 0:
for _, team := range user.TeamMemberships {
for _, wl := range cfg.Cfg.TeamWhiteList {
if team == wl {
log.Debugf("verifyUser: Success! found user.TeamWhiteList in TeamWhiteList: %s for user %s", wl, user.Username)
return true, nil
}
}
}
return false, fmt.Errorf("verifyUser: user.TeamMemberships %s not found in TeamWhiteList: %s for user %s", user.TeamMemberships, cfg.Cfg.TeamWhiteList, user.Username)
// Domains
case len(cfg.Cfg.Domains) != 0:
if domains.IsUnderManagement(user.Email) {
log.Debugf("verifyUser: Success! Email %s found within a %s managed domain", user.Email, cfg.Branding.FullName)
return true, nil
}
return false, fmt.Errorf("verifyUser: Email %s is not within a %s managed domain", user.Email, cfg.Branding.FullName)
// nothing configured, allow everyone through
default:
log.Warn("verifyUser: no domains, whitelist, teamWhitelist or AllowAllUsers configured, any successful auth to the IdP authorizes access")
return true, nil
}
}
func getUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens, opts ...oauth2.AuthCodeOption) error {
return provider.GetUserInfo(r, user, customClaims, ptokens, opts...)
}