128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package google
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"crypto/sha256"
|
|||
|
|
"encoding/base64"
|
|||
|
|
"net/url"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// The challenge has to be the SHA-256 of the verifier, base64url without padding.
|
|||
|
|
// Getting this wrong means Google refuses every exchange with a message that does
|
|||
|
|
// not say which end is at fault.
|
|||
|
|
func TestPKCEChallengeMatchesItsVerifier(t *testing.T) {
|
|||
|
|
p, err := NewPKCE()
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sum := sha256.Sum256([]byte(p.Verifier))
|
|||
|
|
want := base64.RawURLEncoding.EncodeToString(sum[:])
|
|||
|
|
if p.Challenge != want {
|
|||
|
|
t.Errorf("challenge = %q, want %q", p.Challenge, want)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RFC 7636 requires 43–128 characters, and no padding.
|
|||
|
|
if n := len(p.Verifier); n < 43 || n > 128 {
|
|||
|
|
t.Errorf("verifier is %d characters, outside the 43–128 the spec allows", n)
|
|||
|
|
}
|
|||
|
|
if strings.ContainsAny(p.Verifier, "=+/") {
|
|||
|
|
t.Errorf("verifier %q contains characters that are not URL-safe", p.Verifier)
|
|||
|
|
}
|
|||
|
|
if p.State == "" {
|
|||
|
|
t.Error("no state was generated")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Two attempts must not share anything.
|
|||
|
|
other, err := NewPKCE()
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
if other.Verifier == p.Verifier || other.State == p.State {
|
|||
|
|
t.Error("two authorisation attempts produced the same secrets")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestAuthURL(t *testing.T) {
|
|||
|
|
cfg := Config{
|
|||
|
|
ClientID: "client-123",
|
|||
|
|
ClientSecret: "secret",
|
|||
|
|
RedirectURL: "https://todo.example.dk/oauth/gmail/callback",
|
|||
|
|
Scope: "https://www.googleapis.com/auth/gmail.readonly",
|
|||
|
|
}
|
|||
|
|
p, err := NewPKCE()
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
raw := cfg.AuthURL(p)
|
|||
|
|
parsed, err := url.Parse(raw)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("the URL does not parse: %v", err)
|
|||
|
|
}
|
|||
|
|
q := parsed.Query()
|
|||
|
|
|
|||
|
|
checks := map[string]string{
|
|||
|
|
"client_id": cfg.ClientID,
|
|||
|
|
"redirect_uri": cfg.RedirectURL,
|
|||
|
|
"response_type": "code",
|
|||
|
|
"scope": cfg.Scope,
|
|||
|
|
"code_challenge": p.Challenge,
|
|||
|
|
"code_challenge_method": "S256",
|
|||
|
|
"state": p.State,
|
|||
|
|
// Without both of these Google returns no refresh token on a
|
|||
|
|
// re-authorisation, and the connection dies silently an hour later.
|
|||
|
|
"access_type": "offline",
|
|||
|
|
"prompt": "consent",
|
|||
|
|
}
|
|||
|
|
for key, want := range checks {
|
|||
|
|
if got := q.Get(key); got != want {
|
|||
|
|
t.Errorf("%s = %q, want %q", key, got, want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The verifier itself must never leave the server.
|
|||
|
|
if strings.Contains(raw, p.Verifier) {
|
|||
|
|
t.Error("the code verifier was put in the authorisation URL")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The scope is the one thing that differs between the two features signing in
|
|||
|
|
// through this, so it has to come out of the config rather than out of a constant
|
|||
|
|
// somebody could forget to change.
|
|||
|
|
func TestTheScopeComesFromTheConfig(t *testing.T) {
|
|||
|
|
p, err := NewPKCE()
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
calendar := "https://www.googleapis.com/auth/calendar.readonly"
|
|||
|
|
raw := Config{ClientID: "a", Scope: calendar}.AuthURL(p)
|
|||
|
|
|
|||
|
|
parsed, err := url.Parse(raw)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
if got := parsed.Query().Get("scope"); got != calendar {
|
|||
|
|
t.Errorf("scope = %q, want %q — asking for the wrong one is a consent screen "+
|
|||
|
|
"that grants the wrong thing", got, calendar)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestConfigured(t *testing.T) {
|
|||
|
|
full := Config{ClientID: "a", ClientSecret: "b", RedirectURL: "c"}
|
|||
|
|
if !full.Configured() {
|
|||
|
|
t.Error("a complete config is not reported as configured")
|
|||
|
|
}
|
|||
|
|
for _, partial := range []Config{
|
|||
|
|
{},
|
|||
|
|
{ClientID: "a"},
|
|||
|
|
{ClientID: "a", ClientSecret: "b"},
|
|||
|
|
{ClientSecret: "b", RedirectURL: "c"},
|
|||
|
|
} {
|
|||
|
|
if partial.Configured() {
|
|||
|
|
t.Errorf("%+v was reported as configured", partial)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|