git.iko.soy/typeable/oauth2-proxy
on v3.2.0 help sign in
275 lines 6.9 kB history raw
1package providers
3import (
4 "bytes"
5 "crypto/rsa"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io/ioutil"
10 "math/rand"
11 "net/http"
12 "net/url"
13 "time"
15 "github.com/dgrijalva/jwt-go"
16 "gopkg.in/square/go-jose.v2"
19// LoginGovProvider represents an OIDC based Identity Provider
20type LoginGovProvider struct {
21 *ProviderData
23 // TODO (@timothy-spencer): Ideally, the nonce would be in the session state, but the session state
24 // is created only upon code redemption, not during the auth, when this must be supplied.
25 Nonce string
26 AcrValues string
27 JWTKey *rsa.PrivateKey
28 PubJWKURL *url.URL
31// For generating a nonce
32var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
34func randSeq(n int) string {
35 b := make([]rune, n)
36 for i := range b {
37 b[i] = letters[rand.Intn(len(letters))]
38 }
39 return string(b)
42// NewLoginGovProvider initiates a new LoginGovProvider
43func NewLoginGovProvider(p *ProviderData) *LoginGovProvider {
44 p.ProviderName = "login.gov"
46 if p.LoginURL == nil || p.LoginURL.String() == "" {
47 p.LoginURL = &url.URL{
48 Scheme: "https",
49 Host: "secure.login.gov",
50 Path: "/openid_connect/authorize",
51 }
52 }
53 if p.RedeemURL == nil || p.RedeemURL.String() == "" {
54 p.RedeemURL = &url.URL{
55 Scheme: "https",
56 Host: "secure.login.gov",
57 Path: "/api/openid_connect/token",
58 }
59 }
60 if p.ProfileURL == nil || p.ProfileURL.String() == "" {
61 p.ProfileURL = &url.URL{
62 Scheme: "https",
63 Host: "secure.login.gov",
64 Path: "/api/openid_connect/userinfo",
65 }
66 }
67 if p.Scope == "" {
68 p.Scope = "email openid"
69 }
71 return &LoginGovProvider{
72 ProviderData: p,
73 Nonce: randSeq(32),
74 }
77type loginGovCustomClaims struct {
78 Acr string `json:"acr"`
79 Nonce string `json:"nonce"`
80 Email string `json:"email"`
81 EmailVerified bool `json:"email_verified"`
82 GivenName string `json:"given_name"`
83 FamilyName string `json:"family_name"`
84 Birthdate string `json:"birthdate"`
85 AtHash string `json:"at_hash"`
86 CHash string `json:"c_hash"`
87 jwt.StandardClaims
90// checkNonce checks the nonce in the id_token
91func checkNonce(idToken string, p *LoginGovProvider) (err error) {
92 token, err := jwt.ParseWithClaims(idToken, &loginGovCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
93 resp, myerr := http.Get(p.PubJWKURL.String())
94 if myerr != nil {
95 return nil, myerr
96 }
97 if resp.StatusCode != 200 {
98 myerr = fmt.Errorf("got %d from %q", resp.StatusCode, p.PubJWKURL.String())
99 return nil, myerr
100 }
101 body, myerr := ioutil.ReadAll(resp.Body)
102 resp.Body.Close()
103 if myerr != nil {
104 return nil, myerr
105 }
107 var pubkeys jose.JSONWebKeySet
108 myerr = json.Unmarshal(body, &pubkeys)
109 if myerr != nil {
110 return nil, myerr
111 }
112 pubkey := pubkeys.Keys[0]
114 return pubkey.Key, nil
115 })
116 if err != nil {
117 return
118 }
120 claims := token.Claims.(*loginGovCustomClaims)
121 if claims.Nonce != p.Nonce {
122 err = fmt.Errorf("nonce validation failed")
123 return
124 }
125 return
128func emailFromUserInfo(accessToken string, userInfoEndpoint string) (email string, err error) {
129 // query the user info endpoint for user attributes
130 var req *http.Request
131 req, err = http.NewRequest("GET", userInfoEndpoint, nil)
132 if err != nil {
133 return
134 }
135 req.Header.Set("Authorization", "Bearer "+accessToken)
137 resp, err := http.DefaultClient.Do(req)
138 if err != nil {
139 return
140 }
141 var body []byte
142 body, err = ioutil.ReadAll(resp.Body)
143 resp.Body.Close()
144 if err != nil {
145 return
146 }
148 if resp.StatusCode != 200 {
149 err = fmt.Errorf("got %d from %q %s", resp.StatusCode, userInfoEndpoint, body)
150 return
151 }
153 // parse the user attributes from the data we got and make sure that
154 // the email address has been validated.
155 var emailData struct {
156 Email string `json:"email"`
157 EmailVerified bool `json:"email_verified"`
158 }
159 err = json.Unmarshal(body, &emailData)
160 if err != nil {
161 return
162 }
163 if emailData.Email == "" {
164 err = fmt.Errorf("missing email")
165 return
166 }
167 email = emailData.Email
168 if !emailData.EmailVerified {
169 err = fmt.Errorf("email %s not listed as verified", email)
170 return
171 }
172 return
175// Redeem exchanges the OAuth2 authentication token for an ID token
176func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
177 if code == "" {
178 err = errors.New("missing code")
179 return
180 }
182 claims := &jwt.StandardClaims{
183 Issuer: p.ClientID,
184 Subject: p.ClientID,
185 Audience: p.RedeemURL.String(),
186 ExpiresAt: int64(time.Now().Add(time.Duration(5 * time.Minute)).Unix()),
187 Id: randSeq(32),
188 }
189 token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)
190 ss, err := token.SignedString(p.JWTKey)
191 if err != nil {
192 return
193 }
195 params := url.Values{}
196 params.Add("client_assertion", ss)
197 params.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
198 params.Add("code", code)
199 params.Add("grant_type", "authorization_code")
201 var req *http.Request
202 req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
203 if err != nil {
204 return
205 }
206 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
208 var resp *http.Response
209 resp, err = http.DefaultClient.Do(req)
210 if err != nil {
211 return nil, err
212 }
213 var body []byte
214 body, err = ioutil.ReadAll(resp.Body)
215 resp.Body.Close()
216 if err != nil {
217 return
218 }
220 if resp.StatusCode != 200 {
221 err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
222 return
223 }
225 // Get the token from the body that we got from the token endpoint.
226 var jsonResponse struct {
227 AccessToken string `json:"access_token"`
228 IDToken string `json:"id_token"`
229 TokenType string `json:"token_type"`
230 ExpiresIn int64 `json:"expires_in"`
231 }
232 err = json.Unmarshal(body, &jsonResponse)
233 if err != nil {
234 return
235 }
237 // check nonce here
238 err = checkNonce(jsonResponse.IDToken, p)
239 if err != nil {
240 return
241 }
243 // Get the email address
244 var email string
245 email, err = emailFromUserInfo(jsonResponse.AccessToken, p.ProfileURL.String())
246 if err != nil {
247 return
248 }
250 // Store the data that we found in the session state
251 s = &SessionState{
252 AccessToken: jsonResponse.AccessToken,
253 IDToken: jsonResponse.IDToken,
254 ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
255 Email: email,
256 }
257 return
260// GetLoginURL overrides GetLoginURL to add login.gov parameters
261func (p *LoginGovProvider) GetLoginURL(redirectURI, state string) string {
262 var a url.URL
263 a = *p.LoginURL
264 params, _ := url.ParseQuery(a.RawQuery)
265 params.Set("redirect_uri", redirectURI)
266 params.Set("approval_prompt", p.ApprovalPrompt)
267 params.Add("scope", p.Scope)
268 params.Set("client_id", p.ClientID)
269 params.Set("response_type", "code")
270 params.Add("state", state)
271 params.Add("acr_values", p.AcrValues)
272 params.Add("nonce", p.Nonce)
273 a.RawQuery = params.Encode()
274 return a.String()