Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ID Token sign in with Google SDK #3515

Merged
merged 5 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions selfservice/strategy/oidc/provider_apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (

"github.com/coreos/go-oidc"
"github.com/golang-jwt/jwt/v4"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

"github.com/pkg/errors"

Expand All @@ -23,7 +22,7 @@ import (

type ProviderApple struct {
*ProviderGenericOIDC
jwksUrl string
JWKSUrl string
}

func NewProviderApple(
Expand All @@ -36,7 +35,7 @@ func NewProviderApple(
config: config,
reg: reg,
},
jwksUrl: "https://appleid.apple.com/auth/keys",
JWKSUrl: "https://appleid.apple.com/auth/keys",
}
}

Expand Down Expand Up @@ -118,7 +117,7 @@ func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, quer
if err != nil {
return claims, err
}
decodeQuery(query, claims)
a.DecodeQuery(query, claims)

return claims, nil
}
Expand All @@ -127,7 +126,7 @@ func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, quer
// The info is sent as an extra query parameter to the redirect URL.
// See https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
// Note that there's no way to make sure the info hasn't been tampered with.
func decodeQuery(query url.Values, claims *Claims) {
func (a *ProviderApple) DecodeQuery(query url.Values, claims *Claims) {
var user struct {
Name *struct {
FirstName *string `json:"firstName"`
Expand All @@ -154,11 +153,11 @@ func decodeQuery(query url.Values, claims *Claims) {
var _ IDTokenVerifier = new(ProviderApple)

func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
keySet := oidc.NewRemoteKeySet(ctx, a.jwksUrl)
keySet := oidc.NewRemoteKeySet(ctx, a.JWKSUrl)
verifier := oidc.NewVerifier("https://appleid.apple.com", keySet, &oidc.Config{
ClientID: a.config.ClientID,
})
token, err := verifier.Verify(oidc.ClientContext(ctx, otelhttp.DefaultClient), rawIDToken)
token, err := verifier.Verify(oidc.ClientContext(ctx, a.reg.HTTPClient(ctx).HTTPClient), rawIDToken)
if err != nil {
return nil, err
}
Expand Down
109 changes: 38 additions & 71 deletions selfservice/strategy/oidc/provider_apple_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package oidc
package oidc_test

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
Expand All @@ -16,9 +15,11 @@ import (
_ "embed"

"github.com/golang-jwt/jwt/v4"
"github.com/rakutentech/jwk-go/jwk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ory/kratos/internal"
"github.com/ory/kratos/selfservice/strategy/oidc"
)

func TestDecodeQuery(t *testing.T) {
Expand All @@ -27,18 +28,19 @@ func TestDecodeQuery(t *testing.T) {
}

for k, tc := range []struct {
claims *Claims
claims *oidc.Claims
familyName string
givenName string
lastName string
}{
{claims: &Claims{}, familyName: "first", givenName: "first", lastName: "last"},
{claims: &Claims{FamilyName: "fam"}, familyName: "fam", givenName: "first", lastName: "last"},
{claims: &Claims{FamilyName: "fam", GivenName: "giv"}, familyName: "fam", givenName: "giv", lastName: "last"},
{claims: &Claims{FamilyName: "fam", GivenName: "giv", LastName: "las"}, familyName: "fam", givenName: "giv", lastName: "las"},
{claims: &oidc.Claims{}, familyName: "first", givenName: "first", lastName: "last"},
{claims: &oidc.Claims{FamilyName: "fam"}, familyName: "fam", givenName: "first", lastName: "last"},
{claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv"}, familyName: "fam", givenName: "giv", lastName: "last"},
{claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv", LastName: "las"}, familyName: "fam", givenName: "giv", lastName: "las"},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
decodeQuery(query, tc.claims)
a := oidc.NewProviderApple(&oidc.Configuration{}, nil).(*oidc.ProviderApple)
a.DecodeQuery(query, tc.claims)
assert.Equal(t, tc.familyName, tc.claims.FamilyName)
assert.Equal(t, tc.givenName, tc.claims.GivenName)
assert.Equal(t, tc.lastName, tc.claims.LastName)
Expand All @@ -49,41 +51,7 @@ func TestDecodeQuery(t *testing.T) {

}

//go:embed stub/jwk.json
var rawKey []byte

//go:embed stub/jwks_public.json
var publicJWKS []byte

// Just a public key set, to be able to test what happens if an ID token was issued by a different private key.
//
//go:embed stub/jwks_public2.json
var publicJWKS2 []byte

type claims struct {
*jwt.RegisteredClaims
Email string `json:"email"`
}

func createIdToken(t *testing.T, aud string) string {
key := &jwk.KeySpec{}
require.NoError(t, json.Unmarshal(rawKey, key))
token := jwt.NewWithClaims(jwt.SigningMethodRS256, &claims{
RegisteredClaims: &jwt.RegisteredClaims{
Issuer: "https://appleid.apple.com",
Subject: "apple@ory.sh",
Audience: jwt.ClaimStrings{aud},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
Email: "apple@ory.sh",
})
token.Header["kid"] = key.KeyID
s, err := token.SignedString(key.Key)
require.NoError(t, err)
return s
}

func TestVerify(t *testing.T) {
func TestAppleVerify(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write(publicJWKS)
Expand All @@ -93,16 +61,21 @@ func TestVerify(t *testing.T) {
w.WriteHeader(200)
w.Write(publicJWKS2)
}))
t.Run("case=successful verification", func(t *testing.T) {
apple := ProviderApple{
jwksUrl: ts.URL,
ProviderGenericOIDC: &ProviderGenericOIDC{
config: &Configuration{
ClientID: "com.example.app",
},
},
makeClaims := func(aud string) jwt.RegisteredClaims {
return jwt.RegisteredClaims{
Issuer: "https://appleid.apple.com",
Subject: "apple@ory.sh",
Audience: jwt.ClaimStrings{aud},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
}
token := createIdToken(t, "com.example.app")
}
t.Run("case=successful verification", func(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "com.example.app",
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = ts.URL
token := createIdToken(t, makeClaims("com.example.app"))

c, err := apple.Verify(context.Background(), token)
require.NoError(t, err)
Expand All @@ -112,31 +85,25 @@ func TestVerify(t *testing.T) {
})

t.Run("case=fails due to client_id mismatch", func(t *testing.T) {
apple := ProviderApple{
jwksUrl: ts.URL,
ProviderGenericOIDC: &ProviderGenericOIDC{
config: &Configuration{
ClientID: "com.example.app",
},
},
}
token := createIdToken(t, "com.different-example.app")
_, reg := internal.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "com.example.app",
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = ts.URL
token := createIdToken(t, makeClaims("com.different-example.app"))

_, err := apple.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, `oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
})

t.Run("case=fails due to jwks mismatch", func(t *testing.T) {
apple := ProviderApple{
jwksUrl: tsOtherJWKS.URL,
ProviderGenericOIDC: &ProviderGenericOIDC{
config: &Configuration{
ClientID: "com.example.app",
},
},
}
token := createIdToken(t, "com.example.app")
_, reg := internal.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "com.example.app",
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = tsOtherJWKS.URL
token := createIdToken(t, makeClaims("com.example.app"))

_, err := apple.Verify(context.Background(), token)
require.Error(t, err)
Expand Down
28 changes: 28 additions & 0 deletions selfservice/strategy/oidc/provider_google.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package oidc
import (
"context"

"github.com/coreos/go-oidc"
gooidc "github.com/coreos/go-oidc"
"golang.org/x/oauth2"

Expand All @@ -14,6 +15,7 @@ import (

type ProviderGoogle struct {
*ProviderGenericOIDC
JWKSUrl string
}

func NewProviderGoogle(
Expand All @@ -26,6 +28,7 @@ func NewProviderGoogle(
config: config,
reg: reg,
},
JWKSUrl: "https://www.googleapis.com/oauth2/v3/certs",
}
}

Expand Down Expand Up @@ -66,3 +69,28 @@ func (g *ProviderGoogle) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {

return options
}

var _ IDTokenVerifier = new(ProviderGoogle)

func (p *ProviderGoogle) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
keySet := oidc.NewRemoteKeySet(ctx, p.JWKSUrl)
verifier := oidc.NewVerifier("https://accounts.google.com", keySet, &oidc.Config{
ClientID: p.config.ClientID,
})
token, err := verifier.Verify(oidc.ClientContext(ctx, p.reg.HTTPClient(ctx).HTTPClient), rawIDToken)
if err != nil {
return nil, err
}
claims := &Claims{}
if err := token.Claims(claims); err != nil {
return nil, err
}
return claims, nil
}

var _ NonceValidationSkipper = new(ProviderGoogle)

func (a *ProviderGoogle) CanSkipNonce(c *Claims) bool {
// Not all SDKs support nonce validation, so we skip it if no nonce is present in the claims of the ID Token.
return c.Nonce == ""
}
72 changes: 70 additions & 2 deletions selfservice/strategy/oidc/provider_google_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ package oidc_test

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"

"github.com/ory/kratos/internal"
Expand All @@ -17,7 +22,7 @@ import (
)

func TestProviderGoogle_Scope(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
_, reg := internal.NewVeryFastRegistryWithoutDB(t)

p := oidc.NewProviderGoogle(&oidc.Configuration{
Provider: "google",
Expand All @@ -34,7 +39,7 @@ func TestProviderGoogle_Scope(t *testing.T) {
}

func TestProviderGoogle_AccessType(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
_, reg := internal.NewVeryFastRegistryWithoutDB(t)

p := oidc.NewProviderGoogle(&oidc.Configuration{
Provider: "google",
Expand All @@ -53,3 +58,66 @@ func TestProviderGoogle_AccessType(t *testing.T) {
options := p.AuthCodeURLOptions(r)
assert.Contains(t, options, oauth2.AccessTypeOffline)
}

func TestVerify(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write(publicJWKS)
}))

tsOtherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write(publicJWKS2)
}))

makeClaims := func(aud string) jwt.RegisteredClaims {
return jwt.RegisteredClaims{
Issuer: "https://accounts.google.com",
Subject: "apple@ory.sh",
Audience: jwt.ClaimStrings{aud},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
}
}
createProvider := func(jwksUrl string) *oidc.ProviderGoogle {
_, reg := internal.NewVeryFastRegistryWithoutDB(t)
p := oidc.NewProviderGoogle(&oidc.Configuration{
Provider: "google",
ID: "valid",
ClientID: "com.example.app",
ClientSecret: "secret",
Mapper: "file://./stub/hydra.schema.json",
RequestedClaims: nil,
Scope: []string{"email", "profile", "offline_access"},
}, reg).(*oidc.ProviderGoogle)
p.JWKSUrl = jwksUrl
return p
}
t.Run("case=successful verification", func(t *testing.T) {
p := createProvider(ts.URL)
token := createIdToken(t, makeClaims("com.example.app"))

c, err := p.Verify(context.Background(), token)
require.NoError(t, err)
assert.Equal(t, "apple@ory.sh", c.Email)
assert.Equal(t, "apple@ory.sh", c.Subject)
assert.Equal(t, "https://accounts.google.com", c.Issuer)
})

t.Run("case=fails due to client_id mismatch", func(t *testing.T) {
p := createProvider(ts.URL)
token := createIdToken(t, makeClaims("com.different-example.app"))

_, err := p.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, `oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
})

t.Run("case=fails due to jwks mismatch", func(t *testing.T) {
p := createProvider(tsOtherJWKS.URL)
token := createIdToken(t, makeClaims("com.example.app"))

_, err := p.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error())
})
}
Loading
Loading