goic

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2023 License: MIT Imports: 17 Imported by: 1

README

adhocore/goic

Latest Version Software License Go Report Tweet Support

GOIC, Go Open ID Connect, is OpenID connect client library for Golang. It supports the Authorization Code Flow of OpenID Connect specification.

Installation

go get -u github.com/adhocore/goic

Usage

Decide an endpoint (aka URI) in your server where you would like goic to intercept and add OpenID Connect flow. Let's say /auth/o8. Then the provider name follows it. All the OpenID providers that your server should support will need a unique name and each of the providers get a URI like so /auth/o8/<name>. Example:

| Provider | Name | OpenID URI | Revocation | Signout | |----------|------|------------|----------|------------|---------| | Google | google | /auth/o8/google | Yes | No | Facebook | facebook | /auth/o8/facebook | No | No | Microsoft | microsoft | /auth/o8/microsoft | No | Yes | Yahoo | yahoo | /auth/o8/yahoo | Yes | No | Paypal | paypal | /auth/o8/paypal | No | No

Important: All the providers must provide .well-known configurations for OpenID auto discovery.

Get ready with OpenID provider credentials (client id and secret). For Google, check this. To use the example below you need to export GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars.

You also need to configure application domain and redirect URI in the Provider console/dashboard. (redirect URI is same as OpenID URI in above table).

Below is an example for authorization code flow but instead of copy/pasting it entirely you can use it for reference.

package main

import (
	"log"
	"net/http"
	"os"

	"github.com/adhocore/goic"
)

func main() {
	// Init GOIC with a root uri and verbose mode (=true)
	g := goic.New("/auth/o8", true)

	// Register Google provider with name google and its auth URI
	// It will preemptively load well-known config and jwks keys
	p := g.NewProvider("google", "https://accounts.google.com")

	// Configure credentials for Google provider
	p.WithCredential(os.Getenv("GOOGLE_CLIENT_ID"), os.Getenv("GOOGLE_CLIENT_SECRET"))

	// Configure scope
	p.WithScope("openid email profile")

	// Define a callback that will receive token and user info on successful verification
	g.UserCallback(func(t *goic.Token, u *goic.User, w http.ResponseWriter, r *http.Request) {
		// Persist token and user info as you wish! Be sure to check for error in `u.Error` first
		// Use the available `w` and `r` params to show some nice page with message to your user
		// OR redirect them to homepage/dashboard etc

		// However, for the example, here I just dump it in backend console
		log.Println("token: ", t)
		log.Println("user: ", u)

		// and tell the user it is all good:
		_, _ = w.Write([]byte("All good, check backend console"))
	})

	// Listen address for server, 443 for https as OpenID connect mandates it!
	addr := "localhost:443"
	// You need to find a way to run your localhost in HTTPS as well.
	// You may also alias it something like `goic.lvh.me` (lvh is local virtual host)
	// *.lvh.me is automatically mapped to 127.0.0.1 in unix systems.

	// A catch-all dummy handler
	handler := func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(r.Method + " " + r.URL.Path))
	}

	log.Println("Server running on https://localhost")
	log.Println("            Visit https://localhost/auth/o8/google")

	// This is just example (don't copy it)
	useMux := os.Getenv("GOIC_HTTP_MUX") == "1"
	if useMux {
		mux := http.NewServeMux()
		// If you use http mux, wrap your handler with g.MiddlewareHandler
		mux.Handle("/", g.MiddlewareHandler(http.HandlerFunc(handler)))
		server := &http.Server{Addr: addr, Handler: mux}
		log.Fatal(server.ListenAndServeTLS("server.crt", "server.key"))
	} else {
		// If you just use plain simple handler func,
		// wrap your handler with g.MiddlewareFunc
		http.HandleFunc("/", g.MiddlewareFunc(handler))
		log.Fatal(http.ListenAndServeTLS(addr, "server.crt", "server.key", nil))
	}
}
// OR, you can use shorthand syntax to register providers:

g := goic.New("/auth/o8", false)
g.AddProvider(goic.Google.WithCredential(os.Getenv("GOOGLE_CLIENT_ID"), os.Getenv("GOOGLE_CLIENT_SECRET")))
g.AddProvider(goic.Microsoft.WithCredential(os.Getenv("MICROSOFT_CLIENT_ID"), os.Getenv("MICROSOFT_CLIENT_SECRET")))
g.AddProvider(goic.Yahoo.WithCredential(os.Getenv("YAHOO_CLIENT_ID"), os.Getenv("YAHOO_CLIENT_SECRET")))

// ...

After having code like that, build the binary (go build) and run server program (./<binary>).

You need to point Sign in with <provider> button to https://localhost/auth/o8/<provider> for your end user. For example:

<a href="https://localhost/auth/o8/google">Sign in with Google</a>
<a href="https://localhost/auth/o8/microsoft">Sign in with Microsoft</a>
<a href="https://localhost/auth/o8/yahoo">Sign in with Yahoo</a>

The complete flow is managed and handled by GOIC for you and on successful verification, You will be able to receive user and token info in your callback via g.UserCallback! That is where you persist the user data, set some cookie etc.

Check examples directory later for more, as it will be updated when GOIC has new features.

The example and discussion here assume localhost domain so adjust that accordingly for your domains.

Signing out

For signing out you need to manually invoke g.SignOut() from within http context. See the API below. There is also a working example. Note that not all Providers support signing out.

Revocation

To revoke a token manually, invoke g.RevokeToken() from any context. See the API below. There is also a working example. Note that not all Providers support revocation.


GOIC API

GOIC supports full end-to-end for Authorization Code Flow, however if you want to manually interact, here's summary of API:

Supports

Use it to check if a provider is supported.

g := goic.New("/auth/o8", false)
g.NewProvider("abc", "...").WithCredential("...", "...")

g.Supports("abc") // true
g.Supports("xyz") // false
RequestAuth

Manually request authentication from OpenID Provider. Must be called from within http context.

g := goic.New("/auth/o8", false)
p := g.NewProvider("abc", "...").WithCredential("...", "...")

// Generate random unique state and nonce
state, nonce := goic.RandomString(24), goic.RandomString(24)
// You must save them to cookie/session, so it can be retrieved later for crosscheck

// redir is the redirect url in your host for provider of interest
redir := "https://localhost/auth/o8/" + p.Name

// Redirects to provider first and then back to above redir url
// res = http.ResponseWriter, req = *http.Request
err := g.RequestAuth(p, state, nonce, redir, res, req)
Authenticate

Manually attempt to authenticate after the request comes back from OpenID Provider.

g := goic.New("/auth/o8", false)
p := g.NewProvider("abc", "...").WithCredential("...", "...")

// Read openid provider code from query param, and nonce from cookie/session etc
// PS: Validate that the nonce is relevant to the state sent by openid provider
code, nonce := "", ""

// redir is the redirect url in your host for provider of interest
redir := "https://localhost/auth/o8/" + p.Name

tok, err := g.Authenticate(p, code, nonce, redir)
RefreshToken

Use it to request Access token by using refresh token.

g := goic.New("/auth/o8", false)
// ... add providers
old := &goic.Token{RefreshToken: "your refresh token", Provider: goic.Microsoft.Name}
tok, err := g.RefreshToken(old)
// Do something with new tok.AccessToken
Userinfo

Manually request Userinfo by using the token returned by Authentication above.

g := goic.New("/auth/o8", false)
p := g.NewProvider("abc", "...").WithCredential("...", "...")
// ...
tok, err := g.Authenticate(p, code, nonce, redir)
user := g.UserInfo(tok)
err := user.Error
SignOut

Use it to sign out the user from OpenID Provider. Must be called from within http context. Ideally, you would clear the session and logout user from your own system first and then invoke SignOut.

g := goic.New("/auth/o8", false)
p := g.NewProvider("abc", "...").WithCredential("...", "...")
// ...
tok := &goic.Token{AccessToken: "current session token", Provider: p.Name}
err := g.SignOut(tok, "http://some/preconfigured/redir/uri", res, req)
// redir uri is optional
err := g.SignOut(tok, "", res, req)
RevokeToken

Use it to revoke the token so that is incapacitated.

g := goic.New("/auth/o8", false)
p := g.NewProvider("abc", "...").WithCredential("...", "...")
// ...
tok := &goic.Token{AccessToken: "current session token", Provider: p.Name}
err := g.RevokeToken(tok)

Demo

GOIC has been implemented in opensource project adhocore/urlsh:

Provider Name Demo URL
Google google urlssh.xyz/auth/o8/google
Microsoft microsoft urlssh.xyz/auth/o8/microsoft
Yahoo yahoo urlssh.xyz/auth/o8/yahoo

On successful verification your information is echoed back to you as JSON but not saved in server (pinky promise).


TODO

  • Support refresh token grant_type Check #2
  • Tests and more tests
  • Release stable version
  • Support OpenID Implicit Flow Check #3

License

© MIT | 2021-2099, Jitendra Adhikari

Credits

Release managed by please.


Other projects

My other golang projects you might find interesting and useful:

  • gronx - Lightweight, fast and dependency-free Cron expression parser (due checker), task scheduler and/or daemon for Golang (tested on v1.13 and above) and standalone usage.
  • urlsh - URL shortener and bookmarker service with UI, API, Cache, Hits Counter and forwarder using postgres and redis in backend, bulma in frontend; has web and cli client
  • fast - Check your internet speed with ease and comfort right from the terminal
  • chin - A GO lang command line tool to show a spinner as user waits for some long running jobs to finish.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrProviderState is error for invalid request state
	ErrProviderState = fmt.Errorf("goic provider: invalid request state")

	// ErrProviderSupport is error for unsupported provider
	ErrProviderSupport = fmt.Errorf("goic provider: unsupported provider")

	// ErrTokenEmpty is error for empty token
	ErrTokenEmpty = fmt.Errorf("goic id_token: empty token")

	// ErrTokenInvalid is error for invalid token
	ErrTokenInvalid = fmt.Errorf("goic id_token: invalid id_token")

	// ErrRefreshTokenInvalid is error for invalid token
	ErrRefreshTokenInvalid = fmt.Errorf("goic id_token: invalid refresh_token")

	// ErrTokenClaims is error for invalid token claims
	ErrTokenClaims = fmt.Errorf("goic id_token: invalid id_token claims")

	// ErrTokenNonce is error for invalid noce
	ErrTokenNonce = fmt.Errorf("goic id_token: invalid nonce")

	// ErrTokenAud is error for invalid audience
	ErrTokenAud = fmt.Errorf("goic id_token: invalid audience")

	// ErrTokenAlgo is error for unsupported signing algo
	ErrTokenAlgo = fmt.Errorf("goic id_token: unsupported signing algo")

	// ErrTokenKey is error for undetermined signing key
	ErrTokenKey = fmt.Errorf("goic id_token: can't determine signing key")

	// ErrTokenAccessKey is error for invalid access_token
	ErrTokenAccessKey = fmt.Errorf("goic id_token: invalid access_token")

	// ErrSignOutRedir is error for invalid post sign-out redirect uri
	ErrSignOutRedir = fmt.Errorf("goic sign-out: post redirect uri is invalid")
)
View Source
var Facebook = &Provider{
	Name:      "facebook",
	ResType:   "code",
	URL:       "https://www.facebook.com",
	Scope:     "openid email public_profile",
	wellKnown: &WellKnown{TokenURI: "https://graph.facebook.com/v17.0/oauth/access_token"},
}
View Source
var Google = &Provider{
	Name:  "google",
	URL:   "https://accounts.google.com",
	Scope: "openid email profile",
}

Google is ready to use Provider instance

View Source
var Microsoft = &Provider{
	Name:  "microsoft",
	URL:   "https://login.microsoftonline.com/common/v2.0",
	Scope: "openid email profile offline_access",
}

Microsoft is ready to use Provider instance

View Source
var Paypal = &Provider{
	Name:  "paypal",
	URL:   "https://www.paypalobjects.com",
	Scope: "openid email profile",
}

Paypal live provider

View Source
var PaypalSandbox = &Provider{
	Name:    "paypal",
	Sandbox: true,
	URL:     "https://www.paypalobjects.com",
	Scope:   "openid email profile",
}

PaypalSandbox provider

View Source
var Yahoo = &Provider{
	Name:  "yahoo",
	URL:   "https://login.yahoo.com",
	Scope: "openid openid2 email profile",
}

Yahoo provider

Functions

func AuthRedirectURL added in v0.0.14

func AuthRedirectURL(p *Provider, state, nonce, redir string) string

AuthRedirectURL gives the full auth redirect URL for the provider It returns empty string when there is an error

func Base64UrlDecode added in v0.0.5

func Base64UrlDecode(s string) ([]byte, error)

Base64UrlDecode decodes JWT segments with base64 accounting for URL chars

func GetCurve added in v0.0.9

func GetCurve(s string) elliptic.Curve

GetCurve gets the elliptic.Curve from last 3 chars of string s

func ParseExponent

func ParseExponent(es string) int

ParseExponent ParseModulo parses the "e" value of jwks key

func ParseModulo

func ParseModulo(ns string) *big.Int

ParseModulo parses the "n" value of jwks key

func RandomString

func RandomString(len int) string

RandomString generates random string of given length It sets rand seed on each call and returns generated string.

Types

type Goic

type Goic struct {
	URIPrefix string
	// contains filtered or unexported fields
}

Goic is the main program

func New

func New(uri string, verbose bool) *Goic

New gives new GOIC instance

func (*Goic) AddProvider added in v0.0.6

func (g *Goic) AddProvider(p *Provider) *Provider

AddProvider adds a Provider to Goic

func (*Goic) Authenticate

func (g *Goic) Authenticate(p *Provider, codeOrTok, nonce, redir string) (tok *Token, err error)

Authenticate tries to authenticate a user by given code and nonce It is where token is requested and validated

func (*Goic) GetProvider added in v0.0.14

func (g *Goic) GetProvider(name string) *Provider

GetProvider returns Provider by name or nil if not existent

func (*Goic) MiddlewareFunc

func (g *Goic) MiddlewareFunc(next http.HandlerFunc) http.HandlerFunc

MiddlewareFunc is wrapper for http.HandlerFunc that adds OpenID support

func (*Goic) MiddlewareHandler

func (g *Goic) MiddlewareHandler(next http.Handler) http.Handler

MiddlewareHandler is wrapper for http.Handler that adds OpenID support

func (*Goic) NewProvider

func (g *Goic) NewProvider(name, uri string) *Provider

NewProvider adds a new OpenID provider by name It also preloads the well known config and jwks keys

func (*Goic) RefreshToken added in v0.0.10

func (g *Goic) RefreshToken(tok *Token) (*Token, error)

RefreshToken gets new access token using the refresh token

func (*Goic) RequestAuth

func (g *Goic) RequestAuth(p *Provider, state, nonce, redir string, res http.ResponseWriter, req *http.Request) error

RequestAuth is the starting point of OpenID flow

func (*Goic) RevokeToken added in v0.0.12

func (g *Goic) RevokeToken(tok *Token) error

RevokeToken revokes a Token so that it is no longer usable

func (*Goic) SignOut added in v0.0.11

func (g *Goic) SignOut(tok *Token, redir string, res http.ResponseWriter, req *http.Request) error

SignOut signs out the Token from OpenID Provider and then redirects to given URI Redirect URI must be preconfigured in OpenID Provider already

func (*Goic) Supports

func (g *Goic) Supports(name string) bool

Supports checks if a given provider name is supported

func (*Goic) UnsetState added in v0.0.8

func (g *Goic) UnsetState(s string)

UnsetState unsets state from memory

func (*Goic) UserCallback

func (g *Goic) UserCallback(cb UserCallback) *Goic

UserCallback sets a callback for post user verification

func (*Goic) UserInfo

func (g *Goic) UserInfo(tok *Token) *User

UserInfo loads user info when given a Token Error if any is embedded inside User.Error

type Provider

type Provider struct {
	QueryFn func() string

	Name  string
	URL   string
	Scope string

	ResType string
	Sandbox bool
	// contains filtered or unexported fields
}

Provider represents OpenID Connect provider

func (*Provider) AuthBasicHeader added in v0.0.12

func (p *Provider) AuthBasicHeader() string

AuthBasicHeader gives a string ready to use as Authorization header The returned value contains "Basic " prefix already

func (*Provider) CanRevoke added in v0.0.12

func (p *Provider) CanRevoke() bool

CanRevoke checks if token can be revoked for this Provider

func (*Provider) CanSignOut added in v0.0.12

func (p *Provider) CanSignOut() bool

CanSignOut checks if token can be signed out for this Provider

func (*Provider) GetURI added in v0.1.0

func (p *Provider) GetURI(action string) (uri string)

GetURI gets an endpoint for given action

func (*Provider) Is added in v0.1.0

func (p *Provider) Is(name string) bool

Is checks if provider is given type

func (*Provider) SetErr added in v0.1.0

func (p *Provider) SetErr(err error)

SetErr sets last encountered error

func (*Provider) SetQuery added in v0.1.0

func (p *Provider) SetQuery(fn func() string) *Provider

SetQuery sets query func for inital auth request

func (*Provider) WithCredential

func (p *Provider) WithCredential(id, secret string) *Provider

WithCredential sets client id and secret for a Provider

func (*Provider) WithScope

func (p *Provider) WithScope(s string) *Provider

WithScope sets scope for a Provider

type Token

type Token struct {
	Claims       jwt.MapClaims `json:"-"`
	Err          string        `json:"error,omitempty"`
	ErrDesc      string        `json:"error_description,omitempty"`
	IDToken      string        `json:"id_token"`
	AccessToken  string        `json:"access_token,omitempty"`
	RefreshToken string        `json:"refresh_token,omitempty"`
	Provider     string        `json:"provider,omitempty"`
}

Token represents token structure from well known token endpoint

func (*Token) VerifyClaims added in v0.1.0

func (tok *Token) VerifyClaims(nonce, aud string) (err error)

verifyClaims verifies the claims of a Token

type User

type User struct {
	Email         string `json:"email"`
	EmailVerified bool   `json:"email_verified,omitempty"`
	FamilyName    string `json:"family_name,omitempty"`
	GivenName     string `json:"given_name,omitempty"`
	Locale        string `json:"locale,omitempty"`
	Name          string `json:"name"`
	Picture       string `json:"picture,omitempty"`
	Subject       string `json:"sub,omitempty"`
	Error         error  `json:"-"`
}

User represents user from well known user info endpoint

func (*User) FromClaims added in v0.1.0

func (u *User) FromClaims(c jwt.MapClaims) *User

type UserCallback

type UserCallback func(t *Token, u *User, w http.ResponseWriter, r *http.Request)

UserCallback defines signature for post user verification callback

type WellKnown

type WellKnown struct {
	Issuer      string   `json:"issuer"`
	KeysURI     string   `json:"jwks_uri"`
	AuthURI     string   `json:"authorization_endpoint"`
	TokenURI    string   `json:"token_endpoint"`
	UserInfoURI string   `json:"userinfo_endpoint"`
	SignOutURI  string   `json:"end_session_endpoint,omitempty"`
	RevokeURI   string   `json:"revocation_endpoint,omitempty"`
	XRevokeURI  string   `json:"token_revocation_endpoint,omitempty"`
	AlgoSupport []string `json:"id_token_signing_alg_values_supported"`
	// contains filtered or unexported fields
}

WellKnown represents OpenID Connect well-known config

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL