/
mux.go
187 lines (171 loc) · 6.73 KB
/
mux.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
181
182
183
184
185
186
187
package oauth2
import (
"net/http"
"path"
"time"
"github.com/influxdata/chronograf"
)
// Check to ensure AuthMux is an oauth2.Mux
var _ Mux = &AuthMux{}
// TenMinutes is the default length of time to get a response back from the OAuth provider
const TenMinutes = 10 * time.Minute
// NewAuthMux constructs a Mux handler that checks a cookie against the authenticator
func NewAuthMux(p Provider, a Authenticator, t Tokenizer,
basepath string, l chronograf.Logger,
UseIDToken bool, LoginHint string,
client *http.Client, codeExchange CodeExchange,
) *AuthMux {
if codeExchange == nil {
codeExchange = simpleTokenExchange
}
mux := &AuthMux{
Provider: p,
Auth: a,
Tokens: t,
SuccessURL: path.Join(basepath, "/landing"),
AfterLogoutURL: path.Join(basepath, "/"),
FailureURL: path.Join(basepath, "/login"),
Now: DefaultNowTime,
Logger: l,
UseIDToken: UseIDToken,
LoginHint: LoginHint,
CodeExchange: codeExchange,
}
if client != nil {
mux.client = client
}
return mux
}
// AuthMux services an Oauth2 interaction with a provider and browser and
// stores the resultant token in the user's browser as a cookie. The benefit of
// this is that the cookie's authenticity can be verified independently by any
// Chronograf instance as long as the Authenticator has no external
// dependencies (e.g. on a Database).
type AuthMux struct {
Provider Provider // Provider is the OAuth2 service
Auth Authenticator // Auth is used to Authorize after successful OAuth2 callback and Expire on Logout
Tokens Tokenizer // Tokens is used to create and validate OAuth2 "state"
Logger chronograf.Logger // Logger is used to give some more information about the OAuth2 process
SuccessURL string // SuccessURL is redirect location after successful authorization
AfterLogoutURL string // LogoutURL is redirect location after logout
FailureURL string // FailureURL is redirect location after authorization failure
Now func() time.Time // Now returns the current time (for testing)
UseIDToken bool // UseIDToken enables OpenID id_token support
LoginHint string // LoginHint will be included as a parameter during authentication if non-nil
client *http.Client // client is the http client used in oauth exchange.
CodeExchange CodeExchange // helps with CSRF in exchange of token for authorization code
}
// Login returns a handler that redirects to the providers OAuth login.
func (j *AuthMux) Login() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url, err := j.CodeExchange.AuthCodeURL(r.Context(), j)
if err != nil {
j.Logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL).
Error("Internal authentication error: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
})
}
// Callback is used by OAuth2 provider after authorization is granted. If
// granted, Callback will set a cookie with a month-long expiration. It is
// recommended that the value of the cookie be encoded as a JWT because the JWT
// can be validated without the need for saving state. The JWT contains the
// principal's identifier (e.g. email address).
func (j *AuthMux) Callback() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := j.Logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
state := r.FormValue("state")
code := r.FormValue("code")
token, err := j.CodeExchange.ExchangeCodeForToken(r.Context(), state, code, j)
if err != nil {
log.Error("Unable to exchange code for token ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
if token.Extra("id_token") != nil && !j.UseIDToken {
log.Info("found an extra id_token, but option --useidtoken is not set")
}
// if we received an extra id_token, inspect it
var id string
var group string
if j.UseIDToken && token.Extra("id_token") != nil && token.Extra("id_token") != "" {
log.Debug("found an extra id_token")
if provider, ok := j.Provider.(ExtendedProvider); ok {
log.Debug("provider implements PrincipalIDFromClaims()")
tokenString, ok := token.Extra("id_token").(string)
if !ok {
log.Error("cannot cast id_token as string")
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
claims, err := j.Tokens.GetClaims(tokenString)
if err != nil {
log.Error("parsing extra id_token failed:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
log.Debug("found claims: ", claims)
id, err = provider.PrincipalIDFromClaims(claims)
if err != nil {
log.Error("requested claim not found in id_token:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
group, err = provider.GroupFromClaims(claims)
if err != nil {
log.Error("requested claim not found in id_token:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
} else {
log.Debug("provider does not implement PrincipalIDFromClaims()")
}
} else {
// otherwise perform an additional lookup
oauthClient := j.Provider.Config().Client(r.Context(), token)
// Using the token get the principal identifier from the provider
id, err = j.Provider.PrincipalID(oauthClient)
if err != nil {
log.Error("Unable to get principal identifier ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
group, err = j.Provider.Group(oauthClient)
if err != nil {
log.Error("Unable to get OAuth Group", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
}
p := Principal{
Subject: id,
Issuer: j.Provider.Name(),
Group: group,
}
err = j.Auth.Authorize(r.Context(), w, p)
if err != nil {
log.Error("Unable to get add session to response ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
log.Info("User ", id, " is authenticated")
http.Redirect(w, r, j.SuccessURL, http.StatusTemporaryRedirect)
})
}
// Logout handler will expire our authentication cookie and redirect to the successURL
func (j *AuthMux) Logout() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
j.Auth.Expire(w)
http.Redirect(w, r, j.AfterLogoutURL, http.StatusTemporaryRedirect)
})
}