@@ -0,0 +1,290 @@
1+package providers
2+
3+import (
4+ "crypto"
5+ "crypto/rand"
6+ "crypto/rsa"
7+ "encoding/json"
8+ "net/http"
9+ "net/http/httptest"
10+ "net/url"
11+ "testing"
12+ "time"
13+
14+ "github.com/dgrijalva/jwt-go"
15+ "github.com/stretchr/testify/assert"
16+ "gopkg.in/square/go-jose.v2"
17+)
18+
19+type MyKeyData struct {
20+ PubKey crypto.PublicKey
21+ PrivKey *rsa.PrivateKey
22+ PubJWK jose.JSONWebKey
23+}
24+
25+func newLoginGovServer(body []byte) (*url.URL, *httptest.Server) {
26+ s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
27+ rw.Write(body)
28+ }))
29+ u, _ := url.Parse(s.URL)
30+ return u, s
31+}
32+
33+func newLoginGovProvider() (l *LoginGovProvider, serverKey *MyKeyData, err error) {
34+ key, err := rsa.GenerateKey(rand.Reader, 2048)
35+ if err != nil {
36+ return
37+ }
38+ serverKey = &MyKeyData{
39+ PubKey: key.Public(),
40+ PrivKey: key,
41+ PubJWK: jose.JSONWebKey{
42+ Key: key.Public(),
43+ KeyID: "testkey",
44+ Algorithm: string(jose.RS256),
45+ Use: "sig",
46+ },
47+ }
48+
49+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
50+ if err != nil {
51+ return
52+ }
53+
54+ l = NewLoginGovProvider(
55+ &ProviderData{
56+ ProviderName: "",
57+ LoginURL: &url.URL{},
58+ RedeemURL: &url.URL{},
59+ ProfileURL: &url.URL{},
60+ ValidateURL: &url.URL{},
61+ Scope: ""})
62+ l.JWTKey = privateKey
63+ l.Nonce = "fakenonce"
64+ return
65+}
66+
67+func TestLoginGovProviderDefaults(t *testing.T) {
68+ p, _, err := newLoginGovProvider()
69+ assert.NotEqual(t, nil, p)
70+ assert.NoError(t, err)
71+ assert.Equal(t, "login.gov", p.Data().ProviderName)
72+ assert.Equal(t, "https://secure.login.gov/openid_connect/authorize",
73+ p.Data().LoginURL.String())
74+ assert.Equal(t, "https://secure.login.gov/api/openid_connect/token",
75+ p.Data().RedeemURL.String())
76+ assert.Equal(t, "https://secure.login.gov/api/openid_connect/userinfo",
77+ p.Data().ProfileURL.String())
78+ assert.Equal(t, "email openid", p.Data().Scope)
79+}
80+
81+func TestLoginGovProviderOverrides(t *testing.T) {
82+ p := NewLoginGovProvider(
83+ &ProviderData{
84+ LoginURL: &url.URL{
85+ Scheme: "https",
86+ Host: "example.com",
87+ Path: "/oauth/auth"},
88+ RedeemURL: &url.URL{
89+ Scheme: "https",
90+ Host: "example.com",
91+ Path: "/oauth/token"},
92+ ProfileURL: &url.URL{
93+ Scheme: "https",
94+ Host: "example.com",
95+ Path: "/oauth/profile"},
96+ Scope: "profile"})
97+ assert.NotEqual(t, nil, p)
98+ assert.Equal(t, "login.gov", p.Data().ProviderName)
99+ assert.Equal(t, "https://example.com/oauth/auth",
100+ p.Data().LoginURL.String())
101+ assert.Equal(t, "https://example.com/oauth/token",
102+ p.Data().RedeemURL.String())
103+ assert.Equal(t, "https://example.com/oauth/profile",
104+ p.Data().ProfileURL.String())
105+ assert.Equal(t, "profile", p.Data().Scope)
106+}
107+
108+func TestLoginGovProviderSessionData(t *testing.T) {
109+ p, serverkey, err := newLoginGovProvider()
110+ assert.NotEqual(t, nil, p)
111+ assert.NoError(t, err)
112+
113+ // Set up the redeem endpoint here
114+ type loginGovRedeemResponse struct {
115+ AccessToken string `json:"access_token"`
116+ TokenType string `json:"token_type"`
117+ ExpiresIn int64 `json:"expires_in"`
118+ IDToken string `json:"id_token"`
119+ }
120+ expiresIn := int64(60)
121+ type MyCustomClaims struct {
122+ Acr string `json:"acr"`
123+ Nonce string `json:"nonce"`
124+ Email string `json:"email"`
125+ EmailVerified bool `json:"email_verified"`
126+ GivenName string `json:"given_name"`
127+ FamilyName string `json:"family_name"`
128+ Birthdate string `json:"birthdate"`
129+ AtHash string `json:"at_hash"`
130+ CHash string `json:"c_hash"`
131+ jwt.StandardClaims
132+ }
133+ claims := MyCustomClaims{
134+ "http://idmanagement.gov/ns/assurance/loa/1",
135+ "fakenonce",
136+ "timothy.spencer@gsa.gov",
137+ true,
138+ "",
139+ "",
140+ "",
141+ "",
142+ "",
143+ jwt.StandardClaims{
144+ Audience: "Audience",
145+ ExpiresAt: time.Now().Unix() + expiresIn,
146+ Id: "foo",
147+ IssuedAt: time.Now().Unix(),
148+ Issuer: "https://idp.int.login.gov",
149+ NotBefore: time.Now().Unix() - 1,
150+ Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
151+ },
152+ }
153+ idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
154+ signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
155+ assert.NoError(t, err)
156+ body, err := json.Marshal(loginGovRedeemResponse{
157+ AccessToken: "a1234",
158+ TokenType: "Bearer",
159+ ExpiresIn: expiresIn,
160+ IDToken: signedidtoken,
161+ })
162+ assert.NoError(t, err)
163+ var server *httptest.Server
164+ p.RedeemURL, server = newLoginGovServer(body)
165+ defer server.Close()
166+
167+ // Set up the user endpoint here
168+ type loginGovUserResponse struct {
169+ Email string `json:"email"`
170+ EmailVerified bool `json:"email_verified"`
171+ Subject string `json:"sub"`
172+ }
173+ userbody, err := json.Marshal(loginGovUserResponse{
174+ Email: "timothy.spencer@gsa.gov",
175+ EmailVerified: true,
176+ Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
177+ })
178+ assert.NoError(t, err)
179+ var userserver *httptest.Server
180+ p.ProfileURL, userserver = newLoginGovServer(userbody)
181+ defer userserver.Close()
182+
183+ // Set up the PubJWKURL endpoint here used to verify the JWT
184+ var pubkeys jose.JSONWebKeySet
185+ pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
186+ pubjwkbody, err := json.Marshal(pubkeys)
187+ assert.NoError(t, err)
188+ var pubjwkserver *httptest.Server
189+ p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
190+ defer pubjwkserver.Close()
191+
192+ session, err := p.Redeem("http://redirect/", "code1234")
193+ assert.NoError(t, err)
194+ assert.NotEqual(t, session, nil)
195+ assert.Equal(t, "timothy.spencer@gsa.gov", session.Email)
196+ assert.Equal(t, "a1234", session.AccessToken)
197+
198+ // The test ought to run in under 2 seconds. If not, you may need to bump this up.
199+ assert.InDelta(t, session.ExpiresOn.Unix(), time.Now().Unix()+expiresIn, 2)
200+}
201+
202+func TestLoginGovProviderBadNonce(t *testing.T) {
203+ p, serverkey, err := newLoginGovProvider()
204+ assert.NotEqual(t, nil, p)
205+ assert.NoError(t, err)
206+
207+ // Set up the redeem endpoint here
208+ type loginGovRedeemResponse struct {
209+ AccessToken string `json:"access_token"`
210+ TokenType string `json:"token_type"`
211+ ExpiresIn int64 `json:"expires_in"`
212+ IDToken string `json:"id_token"`
213+ }
214+ expiresIn := int64(60)
215+ type MyCustomClaims struct {
216+ Acr string `json:"acr"`
217+ Nonce string `json:"nonce"`
218+ Email string `json:"email"`
219+ EmailVerified bool `json:"email_verified"`
220+ GivenName string `json:"given_name"`
221+ FamilyName string `json:"family_name"`
222+ Birthdate string `json:"birthdate"`
223+ AtHash string `json:"at_hash"`
224+ CHash string `json:"c_hash"`
225+ jwt.StandardClaims
226+ }
227+ claims := MyCustomClaims{
228+ "http://idmanagement.gov/ns/assurance/loa/1",
229+ "badfakenonce",
230+ "timothy.spencer@gsa.gov",
231+ true,
232+ "",
233+ "",
234+ "",
235+ "",
236+ "",
237+ jwt.StandardClaims{
238+ Audience: "Audience",
239+ ExpiresAt: time.Now().Unix() + expiresIn,
240+ Id: "foo",
241+ IssuedAt: time.Now().Unix(),
242+ Issuer: "https://idp.int.login.gov",
243+ NotBefore: time.Now().Unix() - 1,
244+ Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
245+ },
246+ }
247+ idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
248+ signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
249+ assert.NoError(t, err)
250+ body, err := json.Marshal(loginGovRedeemResponse{
251+ AccessToken: "a1234",
252+ TokenType: "Bearer",
253+ ExpiresIn: expiresIn,
254+ IDToken: signedidtoken,
255+ })
256+ assert.NoError(t, err)
257+ var server *httptest.Server
258+ p.RedeemURL, server = newLoginGovServer(body)
259+ defer server.Close()
260+
261+ // Set up the user endpoint here
262+ type loginGovUserResponse struct {
263+ Email string `json:"email"`
264+ EmailVerified bool `json:"email_verified"`
265+ Subject string `json:"sub"`
266+ }
267+ userbody, err := json.Marshal(loginGovUserResponse{
268+ Email: "timothy.spencer@gsa.gov",
269+ EmailVerified: true,
270+ Subject: "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
271+ })
272+ assert.NoError(t, err)
273+ var userserver *httptest.Server
274+ p.ProfileURL, userserver = newLoginGovServer(userbody)
275+ defer userserver.Close()
276+
277+ // Set up the PubJWKURL endpoint here used to verify the JWT
278+ var pubkeys jose.JSONWebKeySet
279+ pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
280+ pubjwkbody, err := json.Marshal(pubkeys)
281+ assert.NoError(t, err)
282+ var pubjwkserver *httptest.Server
283+ p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
284+ defer pubjwkserver.Close()
285+
286+ _, err = p.Redeem("http://redirect/", "code1234")
287+
288+ // The "badfakenonce" in the idtoken above should cause this to error out
289+ assert.Error(t, err)
290+}