16 "github.com/go-jose/go-jose/v3"
17 "github.com/golang-jwt/jwt/v5"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
23// LoginGovProvider represents an OIDC based Identity Provider
24type LoginGovProvider struct {
27 // TODO (@timothy-spencer): Ideally, the nonce would be in the session state, but the session state
28 // is created only upon code redemption, not during the auth, when this must be supplied.
30 JWTKey *rsa.PrivateKey
34var _ Provider = (*LoginGovProvider)(nil)
36// For generating a nonce
37var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
39func randSeq(n int) string {
42 maxInt := big.NewInt(int64(len(letters)))
43 bigN, err := rand.Int(rand.Reader, maxInt)
45 // This should never happen
48 b[i] = letters[bigN.Int64()]
54 loginGovProviderName = "login.gov"
55 loginGovDefaultScope = "email openid"
59 // Default Login URL for LoginGov.
60 // Pre-parsed URL of https://secure.login.gov/openid_connect/authorize.
61 loginGovDefaultLoginURL = &url.URL{
63 Host: "secure.login.gov",
64 Path: "/openid_connect/authorize",
67 // Default Redeem URL for LoginGov.
68 // Pre-parsed URL of https://secure.login.gov/api/openid_connect/token.
69 loginGovDefaultRedeemURL = &url.URL{
71 Host: "secure.login.gov",
72 Path: "/api/openid_connect/token",
75 // Default Profile URL for LoginGov.
76 // Pre-parsed URL of https://graph.loginGov.com/v2.5/me.
77 loginGovDefaultProfileURL = &url.URL{
79 Host: "secure.login.gov",
80 Path: "/api/openid_connect/userinfo",
84// NewLoginGovProvider initiates a new LoginGovProvider
85func NewLoginGovProvider(p *ProviderData, opts options.LoginGovOptions) (*LoginGovProvider, error) {
86 p.setProviderDefaults(providerDefaults{
87 name: loginGovProviderName,
88 loginURL: loginGovDefaultLoginURL,
89 redeemURL: loginGovDefaultRedeemURL,
90 profileURL: loginGovDefaultProfileURL,
91 validateURL: loginGovDefaultProfileURL,
92 scope: loginGovDefaultScope,
94 provider := &LoginGovProvider{
99 if err := provider.configure(opts); err != nil {
100 return nil, fmt.Errorf("could not configure login.gov provider: %v", err)
105func (p *LoginGovProvider) configure(opts options.LoginGovOptions) error {
106 pubJWKURL, err := url.Parse(opts.PubJWKURL)
108 return fmt.Errorf("could not parse Public JWK URL: %v", err)
110 p.PubJWKURL = pubJWKURL
112 // JWT key can be supplied via env variable or file in the filesystem, but not both.
114 case opts.JWTKey != "" && opts.JWTKeyFile != "":
115 return errors.New("cannot set both jwt-key and jwt-key-file options")
116 case opts.JWTKey == "" && opts.JWTKeyFile == "":
117 return errors.New("login.gov provider requires a private key for signing JWTs")
118 case opts.JWTKey != "":
119 // The JWT Key is in the commandline argument
120 signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(opts.JWTKey))
122 return fmt.Errorf("could not parse RSA Private Key PEM: %v", err)
125 case opts.JWTKeyFile != "":
126 // The JWT key is in the filesystem
127 keyData, err := os.ReadFile(opts.JWTKeyFile)
129 return fmt.Errorf("could not read key file: %v", opts.JWTKeyFile)
131 signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
133 return fmt.Errorf("could not parse private key from PEM file: %v", opts.JWTKeyFile)
140type loginGovCustomClaims struct {
141 Acr string `json:"acr"`
142 Nonce string `json:"nonce"`
143 Email string `json:"email"`
144 EmailVerified bool `json:"email_verified"`
145 GivenName string `json:"given_name"`
146 FamilyName string `json:"family_name"`
147 Birthdate string `json:"birthdate"`
148 AtHash string `json:"at_hash"`
149 CHash string `json:"c_hash"`
153// checkNonce checks the nonce in the id_token
154func checkNonce(idToken string, p *LoginGovProvider) (err error) {
155 token, err := jwt.ParseWithClaims(idToken, &loginGovCustomClaims{}, func(_ *jwt.Token) (interface{}, error) {
156 var pubkeys jose.JSONWebKeySet
157 rerr := requests.New(p.PubJWKURL.String()).Do().UnmarshalInto(&pubkeys)
161 return pubkeys.Keys[0].Key, nil
167 claims := token.Claims.(*loginGovCustomClaims)
168 if claims.Nonce != p.Nonce {
169 err = fmt.Errorf("nonce validation failed")
175func emailFromUserInfo(ctx context.Context, accessToken string, userInfoEndpoint string) (string, error) {
176 // parse the user attributes from the data we got and make sure that
177 // the email address has been validated.
178 var emailData struct {
179 Email string `json:"email"`
180 EmailVerified bool `json:"email_verified"`
183 // query the user info endpoint for user attributes
184 err := requests.New(userInfoEndpoint).
186 SetHeader("Authorization", tokenTypeBearer+" "+accessToken).
188 UnmarshalInto(&emailData)
193 email := emailData.Email
195 return "", fmt.Errorf("missing email")
198 if !emailData.EmailVerified {
199 return "", fmt.Errorf("email %s not listed as verified", email)
205// Redeem exchanges the OAuth2 authentication token for an ID token
206func (p *LoginGovProvider) Redeem(ctx context.Context, _, code, codeVerifier string) (*sessions.SessionState, error) {
208 return nil, ErrMissingCode
211 claims := &jwt.RegisteredClaims{
214 Audience: jwt.ClaimStrings{p.RedeemURL.String()},
215 ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
217 token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)
218 ss, err := token.SignedString(p.JWTKey)
223 params := url.Values{}
224 params.Add("client_assertion", ss)
225 params.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
226 params.Add("code", code)
227 params.Add("grant_type", "authorization_code")
228 if codeVerifier != "" {
229 params.Add("code_verifier", codeVerifier)
232 // Get the token from the body that we got from the token endpoint.
233 var jsonResponse struct {
234 AccessToken string `json:"access_token"`
235 IDToken string `json:"id_token"`
236 TokenType string `json:"token_type"`
237 ExpiresIn int64 `json:"expires_in"`
239 err = requests.New(p.RedeemURL.String()).
241 WithMethod(http.MethodPost).
242 WithBody(bytes.NewBufferString(params.Encode())).
243 SetHeader("Content-Type", "application/x-www-form-urlencoded").
245 UnmarshalInto(&jsonResponse)
251 err = checkNonce(jsonResponse.IDToken, p)
256 // Get the email address
258 email, err = emailFromUserInfo(ctx, jsonResponse.AccessToken, p.ProfileURL.String())
263 session := &sessions.SessionState{
264 AccessToken: jsonResponse.AccessToken,
265 IDToken: jsonResponse.IDToken,
269 session.CreatedAtNow()
270 session.ExpiresIn(time.Duration(jsonResponse.ExpiresIn) * time.Second)
275// GetLoginURL overrides GetLoginURL to add login.gov parameters
276func (p *LoginGovProvider) GetLoginURL(redirectURI, state, _ string, extraParams url.Values) string {
277 if len(extraParams["acr_values"]) == 0 {
278 acr := "http://idmanagement.gov/ns/assurance/loa/1"
279 extraParams.Add("acr_values", acr)
281 extraParams.Add("nonce", p.Nonce)
282 a := makeLoginURL(p.ProviderData, redirectURI, state, extraParams)
286// ValidateSession validates the AccessToken
287func (p *LoginGovProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
288 return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
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