13 "github.com/dgrijalva/jwt-go"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
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.
26 JWTKey *rsa.PrivateKey
30var _ Provider = (*LoginGovProvider)(nil)
32// For generating a nonce
33var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
35func randSeq(n int) string {
38 b[i] = letters[rand.Intn(len(letters))]
44 loginGovProviderName = "login.gov"
45 loginGovDefaultScope = "email openid"
49 // Default Login URL for LoginGov.
50 // Pre-parsed URL of https://secure.login.gov/openid_connect/authorize.
51 loginGovDefaultLoginURL = &url.URL{
53 Host: "secure.login.gov",
54 Path: "/openid_connect/authorize",
57 // Default Redeem URL for LoginGov.
58 // Pre-parsed URL of https://secure.login.gov/api/openid_connect/token.
59 loginGovDefaultRedeemURL = &url.URL{
61 Host: "secure.login.gov",
62 Path: "/api/openid_connect/token",
65 // Default Profile URL for LoginGov.
66 // Pre-parsed URL of https://graph.loginGov.com/v2.5/me.
67 loginGovDefaultProfileURL = &url.URL{
69 Host: "secure.login.gov",
70 Path: "/api/openid_connect/userinfo",
74// NewLoginGovProvider initiates a new LoginGovProvider
75func NewLoginGovProvider(p *ProviderData) *LoginGovProvider {
76 p.setProviderDefaults(providerDefaults{
77 name: loginGovProviderName,
78 loginURL: loginGovDefaultLoginURL,
79 redeemURL: loginGovDefaultRedeemURL,
80 profileURL: loginGovDefaultProfileURL,
82 scope: loginGovDefaultScope,
84 return &LoginGovProvider{
90type loginGovCustomClaims struct {
91 Acr string `json:"acr"`
92 Nonce string `json:"nonce"`
93 Email string `json:"email"`
94 EmailVerified bool `json:"email_verified"`
95 GivenName string `json:"given_name"`
96 FamilyName string `json:"family_name"`
97 Birthdate string `json:"birthdate"`
98 AtHash string `json:"at_hash"`
99 CHash string `json:"c_hash"`
103// checkNonce checks the nonce in the id_token
104func checkNonce(idToken string, p *LoginGovProvider) (err error) {
105 token, err := jwt.ParseWithClaims(idToken, &loginGovCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
106 var pubkeys jose.JSONWebKeySet
107 rerr := requests.New(p.PubJWKURL.String()).Do().UnmarshalInto(&pubkeys)
111 return pubkeys.Keys[0].Key, nil
117 claims := token.Claims.(*loginGovCustomClaims)
118 if claims.Nonce != p.Nonce {
119 err = fmt.Errorf("nonce validation failed")
125func emailFromUserInfo(ctx context.Context, accessToken string, userInfoEndpoint string) (string, error) {
126 // parse the user attributes from the data we got and make sure that
127 // the email address has been validated.
128 var emailData struct {
129 Email string `json:"email"`
130 EmailVerified bool `json:"email_verified"`
133 // query the user info endpoint for user attributes
134 err := requests.New(userInfoEndpoint).
136 SetHeader("Authorization", "Bearer "+accessToken).
138 UnmarshalInto(&emailData)
143 email := emailData.Email
145 return "", fmt.Errorf("missing email")
148 if !emailData.EmailVerified {
149 return "", fmt.Errorf("email %s not listed as verified", email)
155// Redeem exchanges the OAuth2 authentication token for an ID token
156func (p *LoginGovProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
158 err = errors.New("missing code")
162 claims := &jwt.StandardClaims{
165 Audience: p.RedeemURL.String(),
166 ExpiresAt: time.Now().Add(5 * time.Minute).Unix(),
169 token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)
170 ss, err := token.SignedString(p.JWTKey)
175 params := url.Values{}
176 params.Add("client_assertion", ss)
177 params.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
178 params.Add("code", code)
179 params.Add("grant_type", "authorization_code")
181 // Get the token from the body that we got from the token endpoint.
182 var jsonResponse struct {
183 AccessToken string `json:"access_token"`
184 IDToken string `json:"id_token"`
185 TokenType string `json:"token_type"`
186 ExpiresIn int64 `json:"expires_in"`
188 err = requests.New(p.RedeemURL.String()).
191 WithBody(bytes.NewBufferString(params.Encode())).
192 SetHeader("Content-Type", "application/x-www-form-urlencoded").
194 UnmarshalInto(&jsonResponse)
200 err = checkNonce(jsonResponse.IDToken, p)
205 // Get the email address
207 email, err = emailFromUserInfo(ctx, jsonResponse.AccessToken, p.ProfileURL.String())
212 created := time.Now()
213 expires := time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second)
215 // Store the data that we found in the session state
216 s = &sessions.SessionState{
217 AccessToken: jsonResponse.AccessToken,
218 IDToken: jsonResponse.IDToken,
226// GetLoginURL overrides GetLoginURL to add login.gov parameters
227func (p *LoginGovProvider) GetLoginURL(redirectURI, state string) string {
228 extraParams := url.Values{}
229 if p.AcrValues == "" {
230 acr := "http://idmanagement.gov/ns/assurance/loa/1"
231 extraParams.Add("acr_values", acr)
233 extraParams.Add("nonce", p.Nonce)
234 a := makeLoginURL(p.ProviderData, redirectURI, state, extraParams)
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