Skip to content
This repository has been archived by the owner on May 2, 2023. It is now read-only.

Commit

Permalink
Implement request authentication with auth0
Browse files Browse the repository at this point in the history
  • Loading branch information
cyakimov committed Apr 16, 2019
1 parent d6023ad commit 11b9b19
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 147 deletions.
110 changes: 110 additions & 0 deletions authentication/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package authentication

import (
"encoding/base64"
"errors"
"github.com/cyakimov/helios/authentication/providers"
log "github.com/sirupsen/logrus"
"net/http"
"time"
)

const CookieName = "Helios_Authorization"
const HeaderName = "Helios-Jwt-Assertion"

var ErrUnauthorized = errors.New("unauthorized request")

type JWTOpts struct {
Secret string
Expiration time.Duration
}

type Helios struct {
provider providers.OAuth2
jwtOpts JWTOpts
}

func NewHeliosAuthentication(provider providers.OAuth2, jwtSecret string, jwtExpiration time.Duration) Helios {
return Helios{
provider: provider,
jwtOpts: JWTOpts{
Secret: jwtSecret,
Expiration: jwtExpiration,
},
}
}

func (helios Helios) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := authenticate(helios.jwtOpts.Secret, r); err != nil {

// dynamically build callback URL based on current domain
callback := "https://" + r.Host + "/.oauth2/callback"

url := helios.provider.GetLoginURL(callback, r.RequestURI)

log.Debugf("Redirecting to %s", url)

http.Redirect(w, r, url, http.StatusTemporaryRedirect)
return
}

log.Println(r.RequestURI)
// Call the next handler, which can be another middleware in the chain, or the final handler.
next.ServeHTTP(w, r)
})
}

func (helios Helios) CallbackHandler(w http.ResponseWriter, r *http.Request) {
// decode and decrypt state to recover original request url
encodedState := r.URL.Query().Get("state")

state, err := base64.StdEncoding.DecodeString(encodedState)
if err != nil {
log.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

profile, err := helios.provider.GetUserProfile(r)
if err != nil {
log.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

log.Debugf("Authorized. Redirecting to %s", string(state))

exp := time.Now().Add(helios.jwtOpts.Expiration)
jwt, err := IssueJWTWithSecret(helios.jwtOpts.Secret, profile.Email, exp)
http.SetCookie(w, &http.Cookie{
Name: CookieName,
Value: jwt,
Expires: exp,
Path: "/",
Secure: true,
})

http.Redirect(w, r, string(state), http.StatusFound)
return
}

func authenticate(jwtSecret string, r *http.Request) error {
// look for Token in both Cookies and Headers
cookie, err := r.Cookie(CookieName)
token := r.Header.Get(HeaderName)

if err == http.ErrNoCookie && token == "" {
return ErrUnauthorized
}

if token == "" {
token = cookie.Value
}

if !ValidateJWTWithSecret(jwtSecret, token) {
return ErrUnauthorized
}

return nil
}
97 changes: 0 additions & 97 deletions authentication/identity.go

This file was deleted.

33 changes: 21 additions & 12 deletions authentication/auth0.go → authentication/providers/auth0.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package authentication
package providers

import (
"context"
Expand All @@ -11,19 +11,19 @@ import (
)

type Auth0Provider struct {
OAuth2Provider
oauth2 oauth2.Config
domain string
OAuth2
oauth2 oauth2.Config
profileURL string
}

func NewAuth0Provider(config OAuth2Config) OAuth2Provider {
func NewAuth0Provider(config OAuth2Config) OAuth2 {
return Auth0Provider{
domain: config.Domain,
profileURL: config.ProfileURL,
oauth2: oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
RedirectURL: config.CallbackURL,
Scopes: []string{"openid", "email_verified", "email"},
RedirectURL: "", // RedirectURL can vary per route host
Endpoint: oauth2.Endpoint{
AuthURL: config.AuthURL,
TokenURL: config.TokenURL,
Expand All @@ -36,14 +36,20 @@ func (provider Auth0Provider) GetUserProfile(r *http.Request) (OIDCProfile, erro
var profile OIDCProfile
code := r.URL.Query().Get("code")

token, err := provider.oauth2.Exchange(context.TODO(), code)
// Auth0 requires callback URL
url := "https://" + r.Host + "/" + r.URL.Path
callback := oauth2.SetAuthURLParam("redirect_uri", url)

// get access token
token, err := provider.oauth2.Exchange(context.TODO(), code, callback)
if err != nil {
log.Error(err)
return profile, ErrCodeExchange
}

// Get user profile
// get user profile
client := provider.oauth2.Client(context.TODO(), token)
resp, err := client.Get(provider.domain + "/userinfo")
resp, err := client.Get(provider.profileURL)
if err != nil {
return profile, ErrProfile
}
Expand All @@ -67,7 +73,10 @@ func (provider Auth0Provider) GetUserProfile(r *http.Request) (OIDCProfile, erro
return profile, nil
}

func (provider Auth0Provider) GetLoginURL(state string) string {
func (provider Auth0Provider) GetLoginURL(callbackURL string, state string) string {
s := base64.StdEncoding.EncodeToString([]byte(state))
return provider.oauth2.AuthCodeURL(s)

callback := oauth2.SetAuthURLParam("redirect_uri", callbackURL)

return provider.oauth2.AuthCodeURL(s, callback)
}
27 changes: 27 additions & 0 deletions authentication/providers/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package providers

import (
"errors"
"net/http"
)

type OAuth2Config struct {
ClientID string
ClientSecret string
AuthURL string
TokenURL string
ProfileURL string
}

type OIDCProfile struct {
Email string
}

type OAuth2 interface {
GetUserProfile(r *http.Request) (OIDCProfile, error)
GetLoginURL(callbackURL, state string) string
}

var ErrCodeExchange = errors.New("error on code exchange")
var ErrProfile = errors.New("error getting user profile")
var ErrNoEmail = errors.New("no email found in user profile")
18 changes: 17 additions & 1 deletion authentication/token.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package authentication

import (
"fmt"
"github.com/dgrijalva/jwt-go"
"time"
)

func IssueJWT(secret string, email string, expires time.Time) (string, error) {
// IssueJWTWithSecret issues and sign a JWT with a secret
func IssueJWTWithSecret(secret, email string, expires time.Time) (string, error) {
key := []byte(secret)

// Create the Claims
Expand All @@ -19,3 +21,17 @@ func IssueJWT(secret string, email string, expires time.Time) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(key)
}

// ValidateJWTWithSecret checks JWT signing algorithm as well the signature
func ValidateJWTWithSecret(secret, tokenString string) bool {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the alg
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

return []byte(secret), nil
})

return err == nil && token != nil && token.Valid
}
16 changes: 10 additions & 6 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,24 @@ routes:
paths:
- path: /
upstream: httpbin
- path: /json
upstream: httpbin
auth_enabled: false

- host: 127.0.0.1
http:
paths:
- path: /
upstream: httpbin
auth_enabled: false

identity:
provider: auth0
client_id: long-hash-here
client_secret: long-hash-here
oauth2:
domain: https://yourtenant.auth0.com
callback: https://localhost/callback
auth_url: https://yourtenant.auth0.com/authorize
token_url: https://yourtenant.auth0.com/oauth/token
profile_url: https://yourtenant.auth0.com/userinfo
state_secret: long-hash-here

jwt:
shared_secret: replace-this-with-a-long-hash
secret: replace-this-with-a-long-hash
expires: 10h
Loading

0 comments on commit 11b9b19

Please sign in to comment.