git.iko.soy/typeable/oauth2-proxy
on master help sign in
341 lines 10 kB history raw
1package providers
3import (
4 "bytes"
5 "context"
6 "crypto"
7 "crypto/rand"
8 "crypto/rsa"
9 "crypto/x509"
10 "encoding/json"
11 "encoding/pem"
12 "net/http"
13 "net/http/httptest"
14 "net/url"
15 "testing"
16 "time"
18 "github.com/go-jose/go-jose/v3"
19 "github.com/golang-jwt/jwt/v5"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
21 . "github.com/onsi/gomega"
22 "github.com/stretchr/testify/assert"
25type MyKeyData struct {
26 PubKey crypto.PublicKey
27 PrivKey *rsa.PrivateKey
28 PubJWK jose.JSONWebKey
31func newLoginGovServer(body []byte) (*url.URL, *httptest.Server) {
32 s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
33 rw.Write(body)
34 }))
35 u, _ := url.Parse(s.URL)
36 return u, s
39func newPrivateKeyBytes() ([]byte, error) {
40 privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
41 if err != nil {
42 return nil, err
43 }
45 keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
46 if err != nil {
47 return nil, err
48 }
50 privateKeyBlock := &pem.Block{
51 Type: "RSA PRIVATE KEY",
52 Bytes: keyBytes,
53 }
54 b := &bytes.Buffer{}
55 if err := pem.Encode(b, privateKeyBlock); err != nil {
56 return nil, err
57 }
59 return b.Bytes(), nil
62func newLoginGovProvider() (*LoginGovProvider, *MyKeyData, error) {
63 key, err := rsa.GenerateKey(rand.Reader, 2048)
64 if err != nil {
65 return nil, nil, err
66 }
67 serverKey := &MyKeyData{
68 PubKey: key.Public(),
69 PrivKey: key,
70 PubJWK: jose.JSONWebKey{
71 Key: key.Public(),
72 KeyID: "testkey",
73 Algorithm: string(jose.RS256),
74 Use: "sig",
75 },
76 }
78 privKey, err := newPrivateKeyBytes()
79 if err != nil {
80 return nil, nil, err
81 }
83 l, err := NewLoginGovProvider(
84 &ProviderData{
85 ProviderName: "",
86 LoginURL: &url.URL{},
87 RedeemURL: &url.URL{},
88 ProfileURL: &url.URL{},
89 ValidateURL: &url.URL{},
90 Scope: ""},
91 options.LoginGovOptions{
92 JWTKey: string(privKey),
93 },
94 )
95 l.Nonce = "fakenonce"
96 return l, serverKey, err
99func TestNewLoginGovProvider(t *testing.T) {
100 g := NewWithT(t)
102 privKey, err := newPrivateKeyBytes()
103 g.Expect(err).ToNot(HaveOccurred())
105 // Test that defaults are set when calling for a new provider with nothing set
106 provider, err := NewLoginGovProvider(&ProviderData{}, options.LoginGovOptions{
107 JWTKey: string(privKey),
108 })
109 g.Expect(err).ToNot(HaveOccurred())
111 providerData := provider.Data()
112 g.Expect(providerData.ProviderName).To(Equal("login.gov"))
113 g.Expect(providerData.LoginURL.String()).To(Equal("https://secure.login.gov/openid_connect/authorize"))
114 g.Expect(providerData.RedeemURL.String()).To(Equal("https://secure.login.gov/api/openid_connect/token"))
115 g.Expect(providerData.ProfileURL.String()).To(Equal("https://secure.login.gov/api/openid_connect/userinfo"))
116 g.Expect(providerData.ValidateURL.String()).To(Equal("https://secure.login.gov/api/openid_connect/userinfo"))
117 g.Expect(providerData.Scope).To(Equal("email openid"))
120func TestLoginGovProviderOverrides(t *testing.T) {
121 privKey, err := newPrivateKeyBytes()
122 assert.NoError(t, err)
124 p, err := NewLoginGovProvider(
125 &ProviderData{
126 LoginURL: &url.URL{
127 Scheme: "https",
128 Host: "example.com",
129 Path: "/oauth/auth"},
130 RedeemURL: &url.URL{
131 Scheme: "https",
132 Host: "example.com",
133 Path: "/oauth/token"},
134 ProfileURL: &url.URL{
135 Scheme: "https",
136 Host: "example.com",
137 Path: "/oauth/profile"},
138 Scope: "profile"},
139 options.LoginGovOptions{
140 JWTKey: string(privKey),
141 })
142 assert.NoError(t, err)
143 assert.NotEqual(t, nil, p)
144 assert.Equal(t, "login.gov", p.Data().ProviderName)
145 assert.Equal(t, "https://example.com/oauth/auth",
146 p.Data().LoginURL.String())
147 assert.Equal(t, "https://example.com/oauth/token",
148 p.Data().RedeemURL.String())
149 assert.Equal(t, "https://example.com/oauth/profile",
150 p.Data().ProfileURL.String())
151 assert.Equal(t, "profile", p.Data().Scope)
154func TestLoginGovProviderSessionData(t *testing.T) {
155 p, serverkey, err := newLoginGovProvider()
156 assert.NotEqual(t, nil, p)
157 assert.NoError(t, err)
159 // Set up the redeem endpoint here
160 type loginGovRedeemResponse struct {
161 AccessToken string `json:"access_token"`
162 TokenType string `json:"token_type"`
163 ExpiresIn int64 `json:"expires_in"`
164 IDToken string `json:"id_token"`
165 }
166 expiresIn := int64(60)
167 type MyCustomClaims struct {
168 Acr string `json:"acr"`
169 Nonce string `json:"nonce"`
170 Email string `json:"email"`
171 EmailVerified bool `json:"email_verified"`
172 GivenName string `json:"given_name"`
173 FamilyName string `json:"family_name"`
174 Birthdate string `json:"birthdate"`
175 AtHash string `json:"at_hash"`
176 CHash string `json:"c_hash"`
177 jwt.RegisteredClaims
178 }
179 claims := MyCustomClaims{
180 "http://idmanagement.gov/ns/assurance/loa/1",
181 "fakenonce",
182 "timothy.spencer@gsa.gov",
183 true,
184 "",
185 "",
186 "",
187 "",
188 "",
189 jwt.RegisteredClaims{
190 Audience: jwt.ClaimStrings{"Audience"},
191 ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiresIn) * time.Second)),
192 IssuedAt: jwt.NewNumericDate(time.Now()),
193 Issuer: "https://idp.int.login.gov",
194 NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Second)),
195 Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
196 },
197 }
198 idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
199 signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
200 assert.NoError(t, err)
201 body, err := json.Marshal(loginGovRedeemResponse{
202 AccessToken: "a1234",
203 TokenType: "Bearer",
204 ExpiresIn: expiresIn,
205 IDToken: signedidtoken,
206 })
207 assert.NoError(t, err)
208 var server *httptest.Server
209 p.RedeemURL, server = newLoginGovServer(body)
210 defer server.Close()
212 // Set up the user endpoint here
213 type loginGovUserResponse struct {
214 Email string `json:"email"`
215 EmailVerified bool `json:"email_verified"`
216 Subject string `json:"sub"`
217 }
218 userbody, err := json.Marshal(loginGovUserResponse{
219 Email: "timothy.spencer@gsa.gov",
220 EmailVerified: true,
221 Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
222 })
223 assert.NoError(t, err)
224 var userserver *httptest.Server
225 p.ProfileURL, userserver = newLoginGovServer(userbody)
226 defer userserver.Close()
228 // Set up the PubJWKURL endpoint here used to verify the JWT
229 var pubkeys jose.JSONWebKeySet
230 pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
231 pubjwkbody, err := json.Marshal(pubkeys)
232 assert.NoError(t, err)
233 var pubjwkserver *httptest.Server
234 p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
235 defer pubjwkserver.Close()
237 session, err := p.Redeem(context.Background(), "http://redirect/", "code1234", "123")
238 assert.NoError(t, err)
239 assert.NotEqual(t, session, nil)
240 assert.Equal(t, "timothy.spencer@gsa.gov", session.Email)
241 assert.Equal(t, "a1234", session.AccessToken)
243 // The test ought to run in under 2 seconds. If not, you may need to bump this up.
244 assert.InDelta(t, session.ExpiresOn.Unix(), time.Now().Unix()+expiresIn, 2)
247func TestLoginGovProviderBadNonce(t *testing.T) {
248 p, serverkey, err := newLoginGovProvider()
249 assert.NotEqual(t, nil, p)
250 assert.NoError(t, err)
252 // Set up the redeem endpoint here
253 type loginGovRedeemResponse struct {
254 AccessToken string `json:"access_token"`
255 TokenType string `json:"token_type"`
256 ExpiresIn int64 `json:"expires_in"`
257 IDToken string `json:"id_token"`
258 }
259 expiresIn := int64(60)
260 type MyCustomClaims struct {
261 Acr string `json:"acr"`
262 Nonce string `json:"nonce"`
263 Email string `json:"email"`
264 EmailVerified bool `json:"email_verified"`
265 GivenName string `json:"given_name"`
266 FamilyName string `json:"family_name"`
267 Birthdate string `json:"birthdate"`
268 AtHash string `json:"at_hash"`
269 CHash string `json:"c_hash"`
270 jwt.RegisteredClaims
271 }
272 claims := MyCustomClaims{
273 "http://idmanagement.gov/ns/assurance/loa/1",
274 "badfakenonce",
275 "timothy.spencer@gsa.gov",
276 true,
277 "",
278 "",
279 "",
280 "",
281 "",
282 jwt.RegisteredClaims{
283 Audience: jwt.ClaimStrings{"Audience"},
284 ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiresIn) * time.Second)),
285 IssuedAt: jwt.NewNumericDate(time.Now()),
286 Issuer: "https://idp.int.login.gov",
287 NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Second)),
288 Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
289 },
290 }
291 idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
292 signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
293 assert.NoError(t, err)
294 body, err := json.Marshal(loginGovRedeemResponse{
295 AccessToken: "a1234",
296 TokenType: "Bearer",
297 ExpiresIn: expiresIn,
298 IDToken: signedidtoken,
299 })
300 assert.NoError(t, err)
301 var server *httptest.Server
302 p.RedeemURL, server = newLoginGovServer(body)
303 defer server.Close()
305 // Set up the user endpoint here
306 type loginGovUserResponse struct {
307 Email string `json:"email"`
308 EmailVerified bool `json:"email_verified"`
309 Subject string `json:"sub"`
310 }
311 userbody, err := json.Marshal(loginGovUserResponse{
312 Email: "timothy.spencer@gsa.gov",
313 EmailVerified: true,
314 Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
315 })
316 assert.NoError(t, err)
317 var userserver *httptest.Server
318 p.ProfileURL, userserver = newLoginGovServer(userbody)
319 defer userserver.Close()
321 // Set up the PubJWKURL endpoint here used to verify the JWT
322 var pubkeys jose.JSONWebKeySet
323 pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
324 pubjwkbody, err := json.Marshal(pubkeys)
325 assert.NoError(t, err)
326 var pubjwkserver *httptest.Server
327 p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
328 defer pubjwkserver.Close()
330 _, err = p.Redeem(context.Background(), "http://redirect/", "code1234", "123")
332 // The "badfakenonce" in the idtoken above should cause this to error out
333 assert.Error(t, err)
336func TestLoginGovProviderGetLoginURL(t *testing.T) {
337 p, _, _ := newLoginGovProvider()
338 result := p.GetLoginURL("http://redirect/", "", "", url.Values{})
339 assert.Contains(t, result, "acr_values="+url.QueryEscape("http://idmanagement.gov/ns/assurance/loa/1"))
340 assert.Contains(t, result, "nonce=fakenonce")