292 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			292 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
| package providers
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/rsa"
 | |
| 	"encoding/json"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"net/url"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/dgrijalva/jwt-go"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"gopkg.in/square/go-jose.v2"
 | |
| )
 | |
| 
 | |
| type MyKeyData struct {
 | |
| 	PubKey  crypto.PublicKey
 | |
| 	PrivKey *rsa.PrivateKey
 | |
| 	PubJWK  jose.JSONWebKey
 | |
| }
 | |
| 
 | |
| func newLoginGovServer(body []byte) (*url.URL, *httptest.Server) {
 | |
| 	s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | |
| 		rw.Write(body)
 | |
| 	}))
 | |
| 	u, _ := url.Parse(s.URL)
 | |
| 	return u, s
 | |
| }
 | |
| 
 | |
| func newLoginGovProvider() (l *LoginGovProvider, serverKey *MyKeyData, err error) {
 | |
| 	key, err := rsa.GenerateKey(rand.Reader, 2048)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	serverKey = &MyKeyData{
 | |
| 		PubKey:  key.Public(),
 | |
| 		PrivKey: key,
 | |
| 		PubJWK: jose.JSONWebKey{
 | |
| 			Key:       key.Public(),
 | |
| 			KeyID:     "testkey",
 | |
| 			Algorithm: string(jose.RS256),
 | |
| 			Use:       "sig",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	l = NewLoginGovProvider(
 | |
| 		&ProviderData{
 | |
| 			ProviderName: "",
 | |
| 			LoginURL:     &url.URL{},
 | |
| 			RedeemURL:    &url.URL{},
 | |
| 			ProfileURL:   &url.URL{},
 | |
| 			ValidateURL:  &url.URL{},
 | |
| 			Scope:        ""})
 | |
| 	l.JWTKey = privateKey
 | |
| 	l.Nonce = "fakenonce"
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func TestLoginGovProviderDefaults(t *testing.T) {
 | |
| 	p, _, err := newLoginGovProvider()
 | |
| 	assert.NotEqual(t, nil, p)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.Equal(t, "login.gov", p.Data().ProviderName)
 | |
| 	assert.Equal(t, "https://secure.login.gov/openid_connect/authorize",
 | |
| 		p.Data().LoginURL.String())
 | |
| 	assert.Equal(t, "https://secure.login.gov/api/openid_connect/token",
 | |
| 		p.Data().RedeemURL.String())
 | |
| 	assert.Equal(t, "https://secure.login.gov/api/openid_connect/userinfo",
 | |
| 		p.Data().ProfileURL.String())
 | |
| 	assert.Equal(t, "email openid", p.Data().Scope)
 | |
| }
 | |
| 
 | |
| func TestLoginGovProviderOverrides(t *testing.T) {
 | |
| 	p := NewLoginGovProvider(
 | |
| 		&ProviderData{
 | |
| 			LoginURL: &url.URL{
 | |
| 				Scheme: "https",
 | |
| 				Host:   "example.com",
 | |
| 				Path:   "/oauth/auth"},
 | |
| 			RedeemURL: &url.URL{
 | |
| 				Scheme: "https",
 | |
| 				Host:   "example.com",
 | |
| 				Path:   "/oauth/token"},
 | |
| 			ProfileURL: &url.URL{
 | |
| 				Scheme: "https",
 | |
| 				Host:   "example.com",
 | |
| 				Path:   "/oauth/profile"},
 | |
| 			Scope: "profile"})
 | |
| 	assert.NotEqual(t, nil, p)
 | |
| 	assert.Equal(t, "login.gov", p.Data().ProviderName)
 | |
| 	assert.Equal(t, "https://example.com/oauth/auth",
 | |
| 		p.Data().LoginURL.String())
 | |
| 	assert.Equal(t, "https://example.com/oauth/token",
 | |
| 		p.Data().RedeemURL.String())
 | |
| 	assert.Equal(t, "https://example.com/oauth/profile",
 | |
| 		p.Data().ProfileURL.String())
 | |
| 	assert.Equal(t, "profile", p.Data().Scope)
 | |
| }
 | |
| 
 | |
| func TestLoginGovProviderSessionData(t *testing.T) {
 | |
| 	p, serverkey, err := newLoginGovProvider()
 | |
| 	assert.NotEqual(t, nil, p)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	// Set up the redeem endpoint here
 | |
| 	type loginGovRedeemResponse struct {
 | |
| 		AccessToken string `json:"access_token"`
 | |
| 		TokenType   string `json:"token_type"`
 | |
| 		ExpiresIn   int64  `json:"expires_in"`
 | |
| 		IDToken     string `json:"id_token"`
 | |
| 	}
 | |
| 	expiresIn := int64(60)
 | |
| 	type MyCustomClaims struct {
 | |
| 		Acr           string `json:"acr"`
 | |
| 		Nonce         string `json:"nonce"`
 | |
| 		Email         string `json:"email"`
 | |
| 		EmailVerified bool   `json:"email_verified"`
 | |
| 		GivenName     string `json:"given_name"`
 | |
| 		FamilyName    string `json:"family_name"`
 | |
| 		Birthdate     string `json:"birthdate"`
 | |
| 		AtHash        string `json:"at_hash"`
 | |
| 		CHash         string `json:"c_hash"`
 | |
| 		jwt.StandardClaims
 | |
| 	}
 | |
| 	claims := MyCustomClaims{
 | |
| 		"http://idmanagement.gov/ns/assurance/loa/1",
 | |
| 		"fakenonce",
 | |
| 		"timothy.spencer@gsa.gov",
 | |
| 		true,
 | |
| 		"",
 | |
| 		"",
 | |
| 		"",
 | |
| 		"",
 | |
| 		"",
 | |
| 		jwt.StandardClaims{
 | |
| 			Audience:  "Audience",
 | |
| 			ExpiresAt: time.Now().Unix() + expiresIn,
 | |
| 			Id:        "foo",
 | |
| 			IssuedAt:  time.Now().Unix(),
 | |
| 			Issuer:    "https://idp.int.login.gov",
 | |
| 			NotBefore: time.Now().Unix() - 1,
 | |
| 			Subject:   "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
 | |
| 		},
 | |
| 	}
 | |
| 	idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
 | |
| 	signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
 | |
| 	assert.NoError(t, err)
 | |
| 	body, err := json.Marshal(loginGovRedeemResponse{
 | |
| 		AccessToken: "a1234",
 | |
| 		TokenType:   "Bearer",
 | |
| 		ExpiresIn:   expiresIn,
 | |
| 		IDToken:     signedidtoken,
 | |
| 	})
 | |
| 	assert.NoError(t, err)
 | |
| 	var server *httptest.Server
 | |
| 	p.RedeemURL, server = newLoginGovServer(body)
 | |
| 	defer server.Close()
 | |
| 
 | |
| 	// Set up the user endpoint here
 | |
| 	type loginGovUserResponse struct {
 | |
| 		Email         string `json:"email"`
 | |
| 		EmailVerified bool   `json:"email_verified"`
 | |
| 		Subject       string `json:"sub"`
 | |
| 	}
 | |
| 	userbody, err := json.Marshal(loginGovUserResponse{
 | |
| 		Email:         "timothy.spencer@gsa.gov",
 | |
| 		EmailVerified: true,
 | |
| 		Subject:       "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
 | |
| 	})
 | |
| 	assert.NoError(t, err)
 | |
| 	var userserver *httptest.Server
 | |
| 	p.ProfileURL, userserver = newLoginGovServer(userbody)
 | |
| 	defer userserver.Close()
 | |
| 
 | |
| 	// Set up the PubJWKURL endpoint here used to verify the JWT
 | |
| 	var pubkeys jose.JSONWebKeySet
 | |
| 	pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
 | |
| 	pubjwkbody, err := json.Marshal(pubkeys)
 | |
| 	assert.NoError(t, err)
 | |
| 	var pubjwkserver *httptest.Server
 | |
| 	p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
 | |
| 	defer pubjwkserver.Close()
 | |
| 
 | |
| 	session, err := p.Redeem(context.Background(), "http://redirect/", "code1234")
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotEqual(t, session, nil)
 | |
| 	assert.Equal(t, "timothy.spencer@gsa.gov", session.Email)
 | |
| 	assert.Equal(t, "a1234", session.AccessToken)
 | |
| 
 | |
| 	// The test ought to run in under 2 seconds.  If not, you may need to bump this up.
 | |
| 	assert.InDelta(t, session.ExpiresOn.Unix(), time.Now().Unix()+expiresIn, 2)
 | |
| }
 | |
| 
 | |
| func TestLoginGovProviderBadNonce(t *testing.T) {
 | |
| 	p, serverkey, err := newLoginGovProvider()
 | |
| 	assert.NotEqual(t, nil, p)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	// Set up the redeem endpoint here
 | |
| 	type loginGovRedeemResponse struct {
 | |
| 		AccessToken string `json:"access_token"`
 | |
| 		TokenType   string `json:"token_type"`
 | |
| 		ExpiresIn   int64  `json:"expires_in"`
 | |
| 		IDToken     string `json:"id_token"`
 | |
| 	}
 | |
| 	expiresIn := int64(60)
 | |
| 	type MyCustomClaims struct {
 | |
| 		Acr           string `json:"acr"`
 | |
| 		Nonce         string `json:"nonce"`
 | |
| 		Email         string `json:"email"`
 | |
| 		EmailVerified bool   `json:"email_verified"`
 | |
| 		GivenName     string `json:"given_name"`
 | |
| 		FamilyName    string `json:"family_name"`
 | |
| 		Birthdate     string `json:"birthdate"`
 | |
| 		AtHash        string `json:"at_hash"`
 | |
| 		CHash         string `json:"c_hash"`
 | |
| 		jwt.StandardClaims
 | |
| 	}
 | |
| 	claims := MyCustomClaims{
 | |
| 		"http://idmanagement.gov/ns/assurance/loa/1",
 | |
| 		"badfakenonce",
 | |
| 		"timothy.spencer@gsa.gov",
 | |
| 		true,
 | |
| 		"",
 | |
| 		"",
 | |
| 		"",
 | |
| 		"",
 | |
| 		"",
 | |
| 		jwt.StandardClaims{
 | |
| 			Audience:  "Audience",
 | |
| 			ExpiresAt: time.Now().Unix() + expiresIn,
 | |
| 			Id:        "foo",
 | |
| 			IssuedAt:  time.Now().Unix(),
 | |
| 			Issuer:    "https://idp.int.login.gov",
 | |
| 			NotBefore: time.Now().Unix() - 1,
 | |
| 			Subject:   "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
 | |
| 		},
 | |
| 	}
 | |
| 	idtoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
 | |
| 	signedidtoken, err := idtoken.SignedString(serverkey.PrivKey)
 | |
| 	assert.NoError(t, err)
 | |
| 	body, err := json.Marshal(loginGovRedeemResponse{
 | |
| 		AccessToken: "a1234",
 | |
| 		TokenType:   "Bearer",
 | |
| 		ExpiresIn:   expiresIn,
 | |
| 		IDToken:     signedidtoken,
 | |
| 	})
 | |
| 	assert.NoError(t, err)
 | |
| 	var server *httptest.Server
 | |
| 	p.RedeemURL, server = newLoginGovServer(body)
 | |
| 	defer server.Close()
 | |
| 
 | |
| 	// Set up the user endpoint here
 | |
| 	type loginGovUserResponse struct {
 | |
| 		Email         string `json:"email"`
 | |
| 		EmailVerified bool   `json:"email_verified"`
 | |
| 		Subject       string `json:"sub"`
 | |
| 	}
 | |
| 	userbody, err := json.Marshal(loginGovUserResponse{
 | |
| 		Email:         "timothy.spencer@gsa.gov",
 | |
| 		EmailVerified: true,
 | |
| 		Subject:       "b2d2d115-1d7e-4579-b9d6-f8e84f4f56ca",
 | |
| 	})
 | |
| 	assert.NoError(t, err)
 | |
| 	var userserver *httptest.Server
 | |
| 	p.ProfileURL, userserver = newLoginGovServer(userbody)
 | |
| 	defer userserver.Close()
 | |
| 
 | |
| 	// Set up the PubJWKURL endpoint here used to verify the JWT
 | |
| 	var pubkeys jose.JSONWebKeySet
 | |
| 	pubkeys.Keys = append(pubkeys.Keys, serverkey.PubJWK)
 | |
| 	pubjwkbody, err := json.Marshal(pubkeys)
 | |
| 	assert.NoError(t, err)
 | |
| 	var pubjwkserver *httptest.Server
 | |
| 	p.PubJWKURL, pubjwkserver = newLoginGovServer(pubjwkbody)
 | |
| 	defer pubjwkserver.Close()
 | |
| 
 | |
| 	_, err = p.Redeem(context.Background(), "http://redirect/", "code1234")
 | |
| 
 | |
| 	// The "badfakenonce" in the idtoken above should cause this to error out
 | |
| 	assert.Error(t, err)
 | |
| }
 |