~mna/webparts-auth0

c6e295fd4cbe767b32603a8ce838229971fc5abe — Martin Angers 1 year, 7 months ago 5cee1ba
implement login and logout flows
1 files changed, 183 insertions(+), 21 deletions(-)

M auth0.go
M auth0.go => auth0.go +183 -21
@@ 2,13 2,24 @@ package auth0

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"

	"git.sr.ht/~mna/webparts/http/httpssn"
	"github.com/coreos/go-oidc"
	"golang.org/x/oauth2"
)

var (
	errInvalidState   = errors.New("invalid state parameter")
	errIDTokenMissing = errors.New("no id_token field in oauth token")
)

// Config configures the auth0 handlers and middleware.
type Config struct {
	// ClientID is the auth0 client ID. See your auth0 dashboard:


@@ 20,25 31,49 @@ type Config struct {
	// ClientSecret is the client secret for your auth0 application. See your auth0
	// dashboard: https://manage.auth0.com/dashboard
	ClientSecret string

	// OauthCallbackURL is the URL that oauth redirects to after the user's
	// login.
	// login. Note that it is your responsibility to mount Endpoint.LoginCallback
	// to the corresponding path in your HTTP server.
	OauthCallbackURL string
	// LogoutReturnURL is the URL to return to after having called the logout
	// page on the auth server.
	LogoutReturnURL string

	// SessionStore is the HTTP session store to use for auth0 authentication.
	SessionStore httpssn.Store
	// SessionName is the name of the session to use in the SessionStore
	// for auth0 authentication.
	SessionName string

	// ErrorFunc is the function to call when an error occurs in the auth flow.
	// If nil, http.Error is used with an http.StatusInternalServerError code
	// The err is always of type auth0.Error and has a Code() int method that
	// returns the recommended HTTP code to use for the response.
	// If nil, http.Error is used with the error's suggested HTTP code
	// and the error's message.
	ErrorFunc func(w http.ResponseWriter, r *http.Request, err error)

	// SessionStore is the HTTP session store to use for auth0 authentication.
	// It stores the login state value as user profile information.
	SessionStore httpssn.Store
}

// Endpoints is the auth0 endpoints provider. It must be created with a call
// to New. It provides HTTP handlers and HTTP middleware methods. The handlers
// can be mounted at a given path like this (where e is a configured Endpoints
// value):
//
//     mux.Handle("/some/path", http.HandlerFunc(e.Login))
//
// And the middleware can be used to wrap a handler.
type Endpoints struct {
	provider *oidc.Provider
	config   oauth2.Config
	store    httpssn.Store
	errfn    func(w http.ResponseWriter, r *http.Request, err error)
	domain          string
	logoutReturnURL string

	provider    *oidc.Provider
	oauthConfig oauth2.Config
	oidcConfig  *oidc.Config

	store   httpssn.Store
	ssnName string

	errfn func(w http.ResponseWriter, r *http.Request, err error)
}

func New(ctx context.Context, conf *Config) (*Endpoints, error) {


@@ 56,29 91,112 @@ func New(ctx context.Context, conf *Config) (*Endpoints, error) {
	}

	return &Endpoints{
		provider: provider,
		config:   oc,
		store:    conf.SessionStore,
		errfn:    errorFunc(conf.ErrorFunc),
		domain:          conf.Domain,
		logoutReturnURL: conf.LogoutReturnURL,

		provider:    provider,
		oauthConfig: oc,
		oidcConfig:  &oidc.Config{ClientID: conf.ClientID},

		store:   conf.SessionStore,
		ssnName: conf.SessionName,

		errfn: errorFunc(conf.ErrorFunc),
	}, nil
}

func errorFunc(fn func(w http.ResponseWriter, r *http.Request, err error)) func(w http.ResponseWriter, r *http.Request, err error) {
	if fn != nil {
		return fn
// Login initiates the oauth2 login flow.
func (e *Endpoints) Login(w http.ResponseWriter, r *http.Request) {
	const stateRawLen = 32
	state := base64RandString(stateRawLen)

	// save the random state nonce in the session
	ssn, err := e.store.Get(r, e.ssnName)
	if err != nil {
		e.errfn(w, r, &Error{err: err, code: http.StatusInternalServerError})
		return
	}
	return func(w http.ResponseWriter, r *http.Request, err error) {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	ssn.Set("state", state)
	if err := e.store.Save(w, r, ssn); err != nil {
		e.errfn(w, r, &Error{err: err, code: http.StatusInternalServerError})
		return
	}
}

func (e *Endpoints) Login(w http.ResponseWriter, r *http.Request) {
	// redirect to the oauth server
	http.Redirect(w, r, e.oauthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)
}

func (e *Endpoints) Logout(w http.ResponseWriter, r *http.Request) {
	lurl, err := url.Parse("https://" + e.domain + "/v2/logout")
	if err != nil {
		e.errfn(w, r, &Error{err: err, code: http.StatusInternalServerError})
		return
	}

	params := make(url.Values)
	params.Add("returnTo", e.logoutReturnURL)
	params.Add("client_id", e.oauthConfig.ClientID)
	lurl.RawQuery = params.Encode()

	http.Redirect(w, r, lurl.String(), http.StatusTemporaryRedirect)
}

// LoginCallback handles the callback from the oauth2 login flow.
func (e *Endpoints) LoginCallback(w http.ResponseWriter, r *http.Request) {
	ssn, err := e.store.Get(r, e.ssnName)
	if err != nil {
		e.errfn(w, r, &Error{err: err, code: http.StatusInternalServerError})
		return
	}
	fmt.Println("URL Query String: >>>> ", r.URL.RawQuery)

	// TODO: is there an error value on the query or something, if access was denied?

	// validate the state field
	qs := r.URL.Query()
	if qs.Get("state") != ssn.Get("state") {
		e.errfn(w, r, &Error{err: errInvalidState, code: http.StatusBadRequest})
		return
	}

	// exchange the authorization code for the token
	token, err := e.oauthConfig.Exchange(r.Context(), qs.Get("code"))
	if err != nil {
		e.errfn(w, r, &Error{err: err, code: http.StatusUnauthorized})
		return
	}

	// get the oidc ID token
	rawIDToken, ok := token.Extra("id_token").(string)
	if !ok {
		e.errfn(w, r, &Error{err: errIDTokenMissing, code: http.StatusInternalServerError})
		return
	}

	// verify the raw ID token and get its parsed content
	idToken, err := e.provider.Verifier(e.oidcConfig).Verify(r.Context(), rawIDToken)
	if err != nil {
		e.errfn(w, r, &Error{err: err, code: http.StatusInternalServerError})
		return
	}

	// get the user's profile
	var profile map[string]interface{}
	if err := idToken.Claims(&profile); err != nil {
		e.errfn(w, r, &Error{err: err, code: http.StatusInternalServerError})
		return
	}

	// TODO: debugging info, print out whatever we received
	b, _ := json.MarshalIndent(idToken, "", "  ")
	fmt.Println(string(b))
	b, _ = json.MarshalIndent(profile, "", "  ")
	fmt.Println(string(b))

	// TODO: save session (but what info?)

	// redirect to target page
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

func (e *Endpoints) RequireAuth(h http.Handler) http.Handler {


@@ 94,3 212,47 @@ func (e *Endpoints) RequireRole(h http.Handler) http.Handler {
		h.ServeHTTP(w, r)
	})
}

// Error wraps an error and provides the recommended HTTP code to use
// for the response.
type Error struct {
	err  error
	code int
}

// Unwrap returns the wrapped error.
func (e *Error) Unwrap() error {
	return e.err
}

// Code returns the recommended HTTP code to use as response for this error.
func (e *Error) Code() int {
	return e.code
}

// Error returns the error message.
func (e *Error) Error() string {
	return e.err.Error()
}

func errorFunc(fn func(w http.ResponseWriter, r *http.Request, err error)) func(w http.ResponseWriter, r *http.Request, err error) {
	if fn != nil {
		return fn
	}
	return func(w http.ResponseWriter, r *http.Request, err error) {
		var ee *Error
		code := http.StatusInternalServerError
		if errors.As(err, &ee) {
			code = ee.Code()
		}
		http.Error(w, err.Error(), code)
	}
}

func base64RandString(rawLen int) string {
	b := make([]byte, rawLen)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	return base64.URLEncoding.EncodeToString(b)
}