8cc5fbf859
* first stab at login.gov provider * fixing bugs now that I think I understand things better * fixing up dependencies * remove some debug stuff * Fixing all dependencies to point at my fork * forgot to hit save on the github rehome here * adding options for setting keys and so on, use JWT workflow instead of PKCE * forgot comma * was too aggressive with search/replace * need JWTKey to be byte array * removed custom refresh stuff * do our own custom jwt claim and store it in the normal session store * golang json types are strange * I have much to learn about golang * fix time and signing key * add http lib * fixed claims up since we don't need custom claims * add libs * forgot ioutil * forgot ioutil * moved back to pusher location * changed proxy github location back so that it builds externally, fixed up []byte stuff, removed client_secret if we are using login.gov * update dependencies * do JWTs properly * finished oidc flow, fixed up tests to work better * updated comments, added test that we set expiresOn properly * got confused with header and post vs get * clean up debug and test dir * add login.gov to README, remove references to my repo * forgot to remove un-needed code * can use sample_key* instead of generating your own * updated changelog * apparently golint wants comments like this * linter wants non-standard libs in a separate grouping * Update options.go Co-Authored-By: timothy-spencer <timothy.spencer@gsa.gov> * Update options.go Co-Authored-By: timothy-spencer <timothy.spencer@gsa.gov> * remove sample_key, improve comments related to client-secret, fix changelog related to PR feedback * github doesn't seem to do gofmt when merging. :-) * update CODEOWNERS * check the nonce * validate the JWT fully * forgot to add pubjwk-url to README * unexport the struct * fix up the err masking that travis found * update nonce comment by request of @JoelSpeed * argh. Thought I'd formatted the merge properly, but apparently not. * fixed test to not fail if the query time was greater than zero
291 lines
8.1 KiB
Go
291 lines
8.1 KiB
Go
package providers
|
|
|
|
import (
|
|
"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("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("http://redirect/", "code1234")
|
|
|
|
// The "badfakenonce" in the idtoken above should cause this to error out
|
|
assert.Error(t, err)
|
|
}
|