Skip to content

janekolszak/idp

Repository files navigation

Identity Provider (IdP) for Hydra

Build Status Code Climate GoDoc Gitter

This is a helper library for handling challenge requests from Hydra, it handles:

  • Storing challenge in a short lived cookie instead of query parameters
  • Passing user's consent to Hydra
  • Retriving keys from Hydra and using them for JWT verification
  • Caching keys and client info

IdP uses Gorilla sessions as the Store. There are many Gorilla sessions backend implementations out there.

About

Let's say we have an Identity Provider with:

  • /login endpoint that accepts Hydra's challenges
  • /consent endpoint that handles getting consent from the user

This is how challenge request could be hadled with the IdP library:

Sequence Diagram

Initialization

There are many implementations of Gorilla sessions. Let's use Postgres as the backend:

import (
	"github.com/janekolszak/idp"
	"github.com/antonlindstrom/pgstore"
	"time"
)

func main() {
	challengeCookieStore, err = pgstore.NewPGStore("postgres://user:pass@address/dbname", []byte("secret"))
	// Return on error

	// Create the IDP
	IDP := idp.NewIDP(&idp.IDPConfig{
		ClusterURL:            /* Hydra's address */,
		ClientID:              /* IDP's client ID */,
		ClientSecret:          /* IDP's client secret */,
		KeyCacheExpiration:    time.Duration(/* Key expiration time */) * time.Second,
		ClientCacheExpiration: time.Duration(/* Client info expiration */) * time.Second,
		CacheCleanupInterval:  time.Duration(/* Cache cleanup interval. Eg. 30 */) * time.Second,
		ChallengeExpiration:   time.Duration(/* Challenge cookie expiration. Eg. 10 */) * time.Minutes,
		ChallengeStore:        challengeCookieStore,
	})

	// Connects with Hydra and fills caches
	err = IDP.Connect(true /*TLS verification*/)
	// Return on error

}

Usage

func HandleChallengeGET(w http.ResponseWriter, r *http.Request) {
	// 0. Render HTML page with a login form
}

func HandleChallengePOST(w http.ResponseWriter, r *http.Request) {
	// 0. Parse and validate login data (username:password, login cookie etc)
	//    Return on error

	// 1. Verify user's credentials (eg. check username:password).
	//    Return on error
	//    Obtain userid

	// 2. Create a Challenge
	challenge, err := IDP.NewChallenge(r, userid)
	//    Return on error

	// 3. Save the Challenge to a cookie with a small TTL
	err = challenge.Save(w, r)
	//    Return on error

	// 4. Redirect to the consent endpoint
}

// Displays Consent screen. Here user agrees for listed scopes
func HandleConsentGET(w http.ResponseWriter, r *http.Request) {

	// 0. Get the Challenge from the cookie
	challenge, err := IDP.GetChallenge(r)
	//    Return on error

	// 1. Display consent screen
	//    Use challenge.User to get user's ID
	//    Use challenge.Scopes to display requested scopes

	// 2. If any error occured delete the Challenge cookie (optional)
	if err != nil {
		err = challenge.Delete(c.Writer, c.Request)
	}

	// 3. Render the HTML consent page
}

func HandleConsentPOST(w http.ResponseWriter, r *http.Request) {
	// 0. Get the Challenge from the cookie
	challenge, err := IDP.GetChallenge(c.Request)
	//    Return on error

	// 1. Parse and validate consent data (eg. form answer=y or list of scopes)
	//    Return on error

	// 2. If user refused access
	err = challenge.RefuseAccess(w, r)
	//    Return

	// 3. If userf agreed to grant access
	err = challenge.GrantAccessToAll(w, r)
	//    Return
}