15 "github.com/dgrijalva/jwt-go"
16 "gopkg.in/square/go-jose.v2"
19// LoginGovProvider represents an OIDC based Identity Provider
20type LoginGovProvider struct {
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.
27 JWTKey *rsa.PrivateKey
31// For generating a nonce
32var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
34func randSeq(n int) string {
37 b[i] = letters[rand.Intn(len(letters))]
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{
49 Host: "secure.login.gov",
50 Path: "/openid_connect/authorize",
53 if p.RedeemURL == nil || p.RedeemURL.String() == "" {
54 p.RedeemURL = &url.URL{
56 Host: "secure.login.gov",
57 Path: "/api/openid_connect/token",
60 if p.ProfileURL == nil || p.ProfileURL.String() == "" {
61 p.ProfileURL = &url.URL{
63 Host: "secure.login.gov",
64 Path: "/api/openid_connect/userinfo",
68 p.Scope = "email openid"
71 return &LoginGovProvider{
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"`
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())
97 if resp.StatusCode != 200 {
98 myerr = fmt.Errorf("got %d from %q", resp.StatusCode, p.PubJWKURL.String())
101 body, myerr := ioutil.ReadAll(resp.Body)
107 var pubkeys jose.JSONWebKeySet
108 myerr = json.Unmarshal(body, &pubkeys)
112 pubkey := pubkeys.Keys[0]
114 return pubkey.Key, nil
120 claims := token.Claims.(*loginGovCustomClaims)
121 if claims.Nonce != p.Nonce {
122 err = fmt.Errorf("nonce validation failed")
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)
135 req.Header.Set("Authorization", "Bearer "+accessToken)
137 resp, err := http.DefaultClient.Do(req)
142 body, err = ioutil.ReadAll(resp.Body)
148 if resp.StatusCode != 200 {
149 err = fmt.Errorf("got %d from %q %s", resp.StatusCode, userInfoEndpoint, body)
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"`
159 err = json.Unmarshal(body, &emailData)
163 if emailData.Email == "" {
164 err = fmt.Errorf("missing email")
167 email = emailData.Email
168 if !emailData.EmailVerified {
169 err = fmt.Errorf("email %s not listed as verified", email)
175// Redeem exchanges the OAuth2 authentication token for an ID token
176func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
178 err = errors.New("missing code")
182 claims := &jwt.StandardClaims{
185 Audience: p.RedeemURL.String(),
186 ExpiresAt: int64(time.Now().Add(time.Duration(5 * time.Minute)).Unix()),
189 token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)
190 ss, err := token.SignedString(p.JWTKey)
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()))
206 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
208 var resp *http.Response
209 resp, err = http.DefaultClient.Do(req)
214 body, err = ioutil.ReadAll(resp.Body)
220 if resp.StatusCode != 200 {
221 err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
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"`
232 err = json.Unmarshal(body, &jsonResponse)
238 err = checkNonce(jsonResponse.IDToken, p)
243 // Get the email address
245 email, err = emailFromUserInfo(jsonResponse.AccessToken, p.ProfileURL.String())
250 // Store the data that we found in the session state
252 AccessToken: jsonResponse.AccessToken,
253 IDToken: jsonResponse.IDToken,
254 ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
260// GetLoginURL overrides GetLoginURL to add login.gov parameters
261func (p *LoginGovProvider) GetLoginURL(redirectURI, state string) string {
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()
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list