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

Fix: Add id_token grant flow #189

Merged
merged 11 commits into from
Nov 1, 2021
189 changes: 188 additions & 1 deletion api/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ package api

import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"

"github.com/coreos/go-oidc/v3/oidc"
jwt "github.com/golang-jwt/jwt"
"github.com/netlify/gotrue/conf"
"github.com/netlify/gotrue/metering"
"github.com/netlify/gotrue/models"
"github.com/netlify/gotrue/storage"
"github.com/pkg/errors"
"github.com/sethvargo/go-password/password"
)

// GoTrueClaims is a struct thats used for JWT claims
Expand Down Expand Up @@ -44,9 +50,49 @@ type RefreshTokenGrantParams struct {
RefreshToken string `json:"refresh_token"`
}

// IdTokenGrantParams are the parameters the IdTokenGrant method accepts
type IdTokenGrantParams struct {
IdToken string `json:"id_token"`
Nonce string `json:"nonce"`
Provider string `json:"provider"`
}

const useCookieHeader = "x-use-cookie"
const useSessionCookie = "session"

func (p *IdTokenGrantParams) getVerifier(ctx context.Context) (*oidc.IDTokenVerifier, error) {
config := getConfig(ctx)
var provider *oidc.Provider
var err error
var oAuthProvider conf.OAuthProviderConfiguration
switch p.Provider {
case "apple":
oAuthProvider = config.External.Apple
provider, err = oidc.NewProvider(ctx, "https://appleid.apple.com")
case "azure":
oAuthProvider = config.External.Azure
provider, err = oidc.NewProvider(ctx, "https://login.microsoftonline.com/common/v2.0")
case "facebook":
oAuthProvider = config.External.Facebook
provider, err = oidc.NewProvider(ctx, "https://www.facebook.com")
case "google":
oAuthProvider = config.External.Google
provider, err = oidc.NewProvider(ctx, "https://accounts.google.com")
default:
return nil, fmt.Errorf("Provider %s doesn't support the id_token grant flow", p.Provider)
}

if err != nil {
return nil, err
}

if !oAuthProvider.Enabled {
return nil, badRequestError("Provider is not enabled")
}

return provider.Verifier(&oidc.Config{ClientID: oAuthProvider.ClientID}), nil
}

// Token is the endpoint for OAuth access token requests
func (a *API) Token(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
Expand All @@ -57,6 +103,8 @@ func (a *API) Token(w http.ResponseWriter, r *http.Request) error {
return a.ResourceOwnerPasswordGrant(ctx, w, r)
case "refresh_token":
return a.RefreshTokenGrant(ctx, w, r)
case "id_token":
return a.IdTokenGrant(ctx, w, r)
default:
return oauthError("unsupported_grant_type", "")
}
Expand Down Expand Up @@ -95,7 +143,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
if models.IsNotFoundError(err) {
return oauthError("invalid_grant", "Invalid login credentials")
}
return internalServerError("Database error finding user").WithInternalError(err)
return internalServerError("Database error querying schema").WithInternalError(err)
}

if params.Email != "" && !user.IsConfirmed() {
Expand Down Expand Up @@ -208,6 +256,145 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
})
}

// IdTokenGrant implements the id_token grant type flow
func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
config := a.getConfig(ctx)
instanceID := getInstanceID(ctx)

params := &IdTokenGrantParams{}

jsonDecoder := json.NewDecoder(r.Body)
if err := jsonDecoder.Decode(params); err != nil {
return badRequestError("Could not read id token grant params: %v", err)
}

if params.IdToken == "" || params.Nonce == "" || params.Provider == "" {
return oauthError("invalid request", "id_token, nonce and provider required")
}

verifier, err := params.getVerifier(ctx)
if err != nil {
return err
}

idToken, err := verifier.Verify(ctx, params.IdToken)
if err != nil {
return badRequestError("%v", err)
}

claims := make(map[string]interface{})
if err := idToken.Claims(&claims); err != nil {
return err
}

// verify nonce to mitigate replay attacks
hashedNonce, ok := claims["nonce"]
if !ok {
return oauthError("invalid request", "missing nonce in id_token")
}
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(params.Nonce)))
if hash != hashedNonce.(string) {
return oauthError("invalid nonce", "").WithInternalMessage("Possible abuse attempt: %v", r)
}

// check if user exists already
email, ok := claims["email"].(string)
if !ok {
return errors.New("Unable to find email associated to provider")
}

aud := claims["aud"].(string)
user, err := models.FindUserByEmailAndAudience(a.db, instanceID, email, aud)

if err != nil && !models.IsNotFoundError(err) {
return internalServerError("Database error querying schema").WithInternalError(err)
}

var token *AccessTokenResponse
err = a.db.Transaction(func(tx *storage.Connection) error {
var terr error
if user == nil {
if config.DisableSignup {
return forbiddenError("Signups not allowed for this instance")
}
password, err := password.Generate(64, 10, 0, false, true)
if err != nil {
return internalServerError("error creating user").WithInternalError(err)
}

signupParams := &SignupParams{
Provider: params.Provider,
Email: email,
Password: password,
Aud: aud,
Data: claims,
}

user, terr = a.signupNewUser(ctx, tx, signupParams)
if terr != nil {
return terr
}
}

if !user.IsConfirmed() {
isEmailVerified := false
emailVerified, ok := claims["email_verified"].(string)
if ok {
isEmailVerified, terr = strconv.ParseBool(emailVerified)
if terr != nil {
return terr
}
}
if (!ok || !isEmailVerified) && !config.Mailer.Autoconfirm {
mailer := a.Mailer(ctx)
referrer := a.getReferrer(r)
if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer); terr != nil {
return internalServerError("Error sending confirmation mail").WithInternalError(terr)
}
return unauthorizedError("Error unverified email")
}

if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); terr != nil {
return terr
}

if terr = triggerEventHooks(ctx, tx, SignupEvent, user, instanceID, config); terr != nil {
return terr
}

if terr = user.Confirm(tx); terr != nil {
return internalServerError("Error updating user").WithInternalError(terr)
}
} else {
if terr := models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, nil); terr != nil {
return terr
}
if terr = triggerEventHooks(ctx, tx, LoginEvent, user, instanceID, config); terr != nil {
return terr
}
}

token, terr = a.issueRefreshToken(ctx, tx, user)
if terr != nil {
return oauthError("server_error", terr.Error())
}
return nil
})

if err != nil {
return err
}

metering.RecordLogin("id_token", user.ID, instanceID)
return sendJSON(w, http.StatusOK, &AccessTokenResponse{
Token: token.Token,
TokenType: token.TokenType,
ExpiresIn: token.ExpiresIn,
RefreshToken: token.RefreshToken,
User: user,
})
}

func generateAccessToken(user *models.User, expiresIn time.Duration, secret string) (string, error) {
claims := &GoTrueClaims{
StandardClaims: jwt.StandardClaims{
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ require (
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62
github.com/beevik/etree v1.1.0
github.com/coreos/go-oidc/v3 v3.0.0 // indirect
github.com/didip/tollbooth/v5 v5.1.1
github.com/fatih/color v1.10.0 // indirect
github.com/go-chi/chi v4.0.2+incompatible
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ=
github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
Expand Down Expand Up @@ -655,6 +657,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
Expand Down Expand Up @@ -903,6 +906,8 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkp
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
Expand Down