oauth2_proxy/providers/logingov_test.go
Tim Spencer 8cc5fbf859 add login.gov provider (#55)
* 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
2019-03-20 13:44:51 +00:00

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)
}