blob: 3b39fba6ecadf7b710dbda4fb67be85b826b7199 [file] [log] [blame]
package endpoints
import (
"errors"
"reflect"
"testing"
"time"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/memcache"
)
var jwtValidTokenObject = signedJWT{
Audience: "my-client-id",
ClientID: "hello-android",
Email: "dude@gmail.com",
Expires: 1370352252,
IssuedAt: 1370348652,
Issuer: "accounts.google.com",
}
// jwtValidTokenTime is a "timestamp" at which jwtValidTokenObject is valid
// (e.g. not expired or something)
var jwtValidTokenTime = time.Date(2013, 6, 4, 13, 24, 15, 0, time.UTC)
// header: {"alg": "RS256", "typ": "JWT"}
// payload:
// {
// "aud": "my-client-id",
// "azp": "hello-android",
// "email": "dude@gmail.com",
// "exp": 1370352252,
// "iat": 1370348652,
// "iss": "accounts.google.com"
// }
// issued at 2013-06-04 14:24:12 UTC
// expires at 2013-06-04 15:24:12 UTC
const jwtValidTokenString = ("eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9." +
"eyJhdWQiOiAibXktY2xpZW50LWlkIiwgImlzcyI6ICJhY2NvdW50cy5nb29nbGUuY29tIiwg" +
"ImV4cCI6IDEzNzAzNTIyNTIsICJhenAiOiAiaGVsbG8tYW5kcm9pZCIsICJpYXQiOiAxMzcw" +
"MzQ4NjUyLCAiZW1haWwiOiAiZHVkZUBnbWFpbC5jb20ifQ." +
"sv7l0v_u6DmVe7s-hg8Q5LOYXNCdUBR7efnvQ4ns6IfBFZ71yPvWfwOYqZuYGQ0a9V5CfR0r" +
"TfNlXVEpW5NE9rZy8hFiZkHBE30yPDti6PUUtT1bZST1VPFnIvSHobcUj-QPBTRC1Df86Vv0" +
"Jmx4yowL1z3Yhe0Zh1WcvUUPG9sKJt8_-qKAv9QeeCMveBYpRSh6JvoU_qUKxPTjOOLvQiqV" +
"4NiNjJ3sDN0P4BHJc3VcqB-SFd7kMRgQy1Fq-NHKN5-T2x4gxPwUy9GvHOftxY47B1NtJ9Q5" +
"KtSui9uXdyBNJnt0xcIT5CcQYkVLoeCldxpSfwfA2kyfuJKBeiQnSA")
// same header and payload, only encoded with a random private key
// (generate one with "openssl genrsa 2048 > dummy.key")
const jwtInvalidKeyToken = ("eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9." +
"eyJhdWQiOiAibXktY2xpZW50LWlkIiwgImlzcyI6ICJhY2NvdW50cy5nb29nbGUuY29tIiwg" +
"ImV4cCI6IDEzNzAzNTIyNTIsICJhenAiOiAiaGVsbG8tYW5kcm9pZCIsICJpYXQiOiAxMzcw" +
"MzQ4NjUyLCAiZW1haWwiOiAiZHVkZUBnbWFpbC5jb20ifQ." +
"PatagaopzqOe_LqM4rddJHKaZ-l2bacN3Lsj2t15c_iZRzjgFXlC6CsR64SaHSdC-wxde3wu" +
"OKKRWZPZA2Zr03TRUMB_iLJDs2Gg4dUsEsVrbZkTzrGcmejrHIIA1wP0hIM1COBIo6bYr9Vz" +
"UBDBR4tlq8kRgNdCmHXRrR1u4ITSFin3skRE6xkIFXmswI4lzpfWGD2f4jsnH8HDu5K-9X3Q" +
"OhUgKUL9Hlz5z2PtLMGl0xXzFNSPoWAPPNZkNJlPUfLKL7QnJdWu_ieQ1L0xUWHcGvb76lgD" +
"AbihLmbYrX0DeIMl6a_n4wKLRzoVv7qz9KlH-RbMjebudwPRnU-yeg")
// header: {"alg": "HS256", "typ": "JWT"}
// same payload.
const jwtInvalidAlgToken = ("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9." +
"Intcblx0XCJhdWRcIjogXCJteS1jbGllbnQtaWRcIlxuXHRcImF6cFwiOiBcImhlbGxvLWFuZ" +
"HJvaWRcIlxuXHRcImVtYWlsXCI6IFwiZHVkZUBnbWFpbC5jb21cIlxuXHRcImV4cFwiOiAxMz" +
"cwMzUyMjUyXG5cdFwiaWF0XCI6IDEzNzAzNDg2NTJcblx0XCJpc3NcIjogXCJhY2NvdW50cy5" +
"nb29nbGUuY29tXCJcbn0i." +
"ylh77ZOrr_Bkd3iFZRrQNcf_GCjCpUtcdhWz3AOLWUA")
// Private key for testing.
// Used to create exponent, modulus and sign JWT token.
const privateKeyPem = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
-----END RSA PRIVATE KEY-----`
// openssl rsa -in private.pem -noout -text
const googCerts = `{
"keyvalues": [{
"algorithm": "RSA",
"modulus": "` +
"AOHo9Ke20Oy/6+K1RlM/UTeFUFqHo9QYNZmFiA2LJbyq5NYKY+8GYJHZjO6FStjn4" +
"K0o/xQk5CJyj6mgkdIA+yvNKvpiwazdz1JA6yUGab0WSH39/8ZlVNVnq7kCxSY5xW" +
"9nartLPxWP9pFoibQQmpF71xDm+832yDbmsi/jErAhknVWLEpYkuakKYMg2qfvG+X" +
"6V2VSZKvdoa3DbW5IO4QmFCXM7fJ/YXAkyQn1KQKb2hUNfKXvs6cpePls5D+uiNvI" +
"Z4jEMlJQNYQYrJE+6oqLqtJvCDqHYb9n2oEmSozk01NsZEgXP/38t2abC+1qoLGUu" +
"0PtFUSvICn6pMYOTmE=" + `",
"exponent": "AQAB",
"keyid": "goog-123"
}]
}`
func TestVerifySignedJWT(t *testing.T) {
r, _, closer := newTestRequest(t, "GET", "/", nil)
defer closer()
nc, err := appengine.Namespace(appengine.NewContext(r), certNamespace)
if err != nil {
t.Fatal(err)
}
item := &memcache.Item{Key: DefaultCertURI, Value: []byte(googCerts)}
if err := memcache.Set(nc, item); err != nil {
t.Fatal(err)
}
tts := []struct {
token string
now time.Time
want *signedJWT
}{
{jwtValidTokenString, jwtValidTokenTime, &jwtValidTokenObject},
{jwtValidTokenString, jwtValidTokenTime.Add(time.Hour * 24), nil},
{jwtValidTokenString, jwtValidTokenTime.Add(-time.Hour * 24), nil},
{jwtInvalidKeyToken, jwtValidTokenTime, nil},
{jwtInvalidAlgToken, jwtValidTokenTime, nil},
{"invalid.token", jwtValidTokenTime, nil},
{"another.invalid.token", jwtValidTokenTime, nil},
}
ec := NewContext(r)
for i, tt := range tts {
jwt, err := verifySignedJWT(ec, tt.token, tt.now.Unix())
switch {
case err != nil && tt.want != nil:
t.Errorf("%d: verifySignedJWT(%q, %d) = %v; want %#v",
i, tt.token, tt.now.Unix(), err, tt.want)
case err == nil && tt.want == nil:
t.Errorf("%d: verifySignedJWT(%q, %d) = %#v; want error",
i, tt.token, tt.now.Unix(), jwt)
case err == nil && tt.want != nil:
if !reflect.DeepEqual(jwt, tt.want) {
t.Errorf("%d: verifySignedJWT(%q, %d) = %v; want %#v",
i, tt.token, tt.now.Unix(), jwt, tt.want)
}
}
}
}
func TestVerifyParsedToken(t *testing.T) {
const (
goog = "accounts.google.com"
clientID = "my-client-id"
email = "dude@gmail.com"
)
audiences := []string{clientID, "hello-android"}
clientIDs := []string{clientID}
tts := []struct {
issuer, audience, clientID, email string
valid bool
}{
{goog, clientID, clientID, email, true},
{goog, "hello-android", clientID, email, true},
{goog, "invalid", clientID, email, false},
{goog, clientID, "invalid", email, false},
{goog, clientID, clientID, "", false},
{"", clientID, clientID, email, false},
}
r, _, closer := newTestRequest(t, "GET", "/", nil)
defer closer()
c := NewContext(r)
for i, tt := range tts {
jwt := signedJWT{
Issuer: tt.issuer,
Audience: tt.audience,
ClientID: tt.clientID,
Email: tt.email,
}
res := verifyParsedToken(c, jwt, audiences, clientIDs)
if res != tt.valid {
t.Errorf("%d: verifyParsedToken(%#v, %v, %v) = %v; want %v",
i, jwt, audiences, clientIDs, res, tt.valid)
}
}
}
func TestCurrentIDTokenUser(t *testing.T) {
jwtOrigParser := jwtParser
defer func() {
jwtParser = jwtOrigParser
}()
r, _, closer := newTestRequest(t, "GET", "/", nil)
defer closer()
c := NewContext(r)
aud := []string{jwtValidTokenObject.Audience, jwtValidTokenObject.ClientID}
azp := []string{jwtValidTokenObject.ClientID}
jwtUnacceptedToken := signedJWT{
Audience: "my-other-client-id",
ClientID: "my-other-client-id",
Email: "me@gmail.com",
Expires: 1370352252,
IssuedAt: 1370348652,
Issuer: "accounts.google.com",
}
tts := []struct {
token *signedJWT
wantEmail string
}{
{&jwtValidTokenObject, jwtValidTokenObject.Email},
{&jwtUnacceptedToken, ""},
{nil, ""},
}
var currToken *signedJWT
jwtParser = func(context.Context, string, int64) (*signedJWT, error) {
if currToken == nil {
return nil, errors.New("Fake verification failed")
}
return currToken, nil
}
for i, tt := range tts {
currToken = tt.token
user, err := currentIDTokenUser(c,
jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix())
switch {
case tt.wantEmail != "" && err != nil:
t.Errorf("%d: currentIDTokenUser(%q, %v, %v, %d) = %v; want email = %q",
i, jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix(), err, tt.wantEmail)
case tt.wantEmail == "" && err == nil:
t.Errorf("%d: currentIDTokenUser(%q, %v, %v, %d) = %#v; want error",
i, jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix(), user)
case err == nil && tt.wantEmail != user.Email:
t.Errorf("%d: currentIDTokenUser(%q, %v, %v, %d) = %#v; want email = %q",
i, jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix(), user, tt.wantEmail)
}
}
}