Skip to main content

Go Guide

This guide demonstrates how to implement authentication in a traditional Go web application using Hanko Identity. We will build a simple example application that allows a user log in, log out and view profile information about the user. You can find the complete code for the example on GitHub

Communication between the application and Hanko Identity is based on the OpenID Connect protocol, which is an authentication layer built on top of the OAuth2 Authorization Framework. Although knowledge about these protocols is helpful, you do not need to know the ins and outs of these protocols to get started with this guide. We will try to keep it simple to get you up and running with Hanko Identity in no time.

Configure your Hanko Identity project

In order for your application to communicate with Hanko Identity you first need to obtain some data about your Hanko Identity tenant and your application from the Hanko Console. If you haven't done so already, register with Hanko and create a Hanko Identity project for your organization. See also the Configuration section in this documentation.

Get your Hanko Identity tenant URL

You need to retrieve your Hanko Identity tenant URL from the Hanko Console. When creating a Hanko Identity project a default domain will be generated. You can view your Identity tenant domain on the dashboard of your project.

Get Your Client Credentials

To connect your application to Hanko Identity you first need to register your application as an OAuth 2.0/OpenID Connect client through the "Your apps" settings of your project. In the "Your apps" panel, click the "Connect app" button. Provide the necessary information in the shown wizard - make sure you select "Web app" as your application type (you can read more about application configuration here and the Console will display your OAuth 2.0/OpenID Connect client credentials (i.e. Client ID and Client Secret) for your application.

note

The Client Secret is shown on application registration only, so make sure to write it down.

Configure Login Redirect URL

The login redirect URL of your application is where Hanko Identity redirects the user after authentication. The URL must be registered in your application settings. When initially connecting our application with Hanko Identity you should have already registered a login redirect URL. If you want to change the URL, go to the "Your apps" section of your project settings select your application in the "Your apps" panel. In the "OIDC client settings" panel, add a URL using the "Login Redirect URL" input. The redirect URL is required, so if the URL is not set, users won't be able to log in to the application.

note

When using the example project, the URL you need to add as the "Login Redirect URL" in your application settings in the Hanko Console is http://localhost:8080/login/callback.

Configure Post-Logout Redirect URL

The post-logout redirect URL of your application is where the user is returned to after logging out with Hanko Identity. The URL must be registered in the Hanko Console. In the "Your apps" section of your project settings select your application in the "Your apps" panel. In the "OIDC client settings" panel, add the URL using the "Logout Redirect URL" input. When implementing the logout handler of your application, we will use this logout redirect URL as a query parameter in the request to the Hanko Identity logout endpoint.

note

When using the example project the logout redirect URL you need to add as the "Post-logout redirect URL" in your application settings Hanko Console is http://localhost:8080.

Configure token endpoint authentication method

The application settings in the Console also allow you to specify the authentication method to use with the token endpoint. The OAuth 2.0/ OIDC client we will use leverages the golang.org/x/oauth2 package's auto-detection of the authentication method, so for this example it does not matter whether you configure it with the client_secret_basic or client_secret_post.

Configure Go to Use Hanko

Configure your application

Start off by providing the data gathered in the previous section as configuration to our application, e.g. through a .env file:

# The Hanko Identity Tenant URL.
# If you're using a Custom Domain, be sure to set this URL to a value that uses the custom domain instead.
WARNING: URL MUST HAVE A TRAILING SLASH AT THE END
OIDC_PROVIDER_URL={YOUR_HANKO_IDENTITY_TENANT_URL}/

# Your application's OIDC Client ID.
OIDC_CLIENT_ID={YOUR_APPLICATION_CLIENT_ID}

# Your application's OIDC Client Secret.
OIDC_CLIENT_SECRET={YOUR_APPLICATION_CLIENT_SECRET}

# Your application's callback URL.
OIDC_LOGIN_REDIRECT_URL=http://localhost:8080/login/callback

Read the configuration using a Go library of your choice. The example application uses the github.com/spf13/viper library.

Load in configuration (click to expand)
config/conf.go
package config

import (
"log"

"github.com/go-playground/validator/v10"
"github.com/pkg/errors"
"github.com/spf13/viper"
)

type Config struct {
ClientId string `mapstructure:"OIDC_CLIENT_ID" validate:"required"`
ClientSecret string `mapstructure:"OIDC_CLIENT_SECRET" validate:"required"`
RedirectURL string `mapstructure:"OIDC_LOGIN_REDIRECT_URL" validate:"required"`
ProviderURL string `mapstructure:"OIDC_PROVIDER_URL" validate:"required"`
}

func Load(path string) (config Config) {
viper.AddConfigPath(path)
viper.SetConfigName("app")
viper.SetConfigType("env")
viper.AutomaticEnv()

if err := viper.ReadInConfig(); err != nil {
log.Fatal(errors.Wrap(err, "config: read configuration"))
}

if err := viper.Unmarshal(&config); err != nil {
log.Fatal(errors.Wrap(err, "config: unmarshal configuration"))
}

if err := validator.New().Struct(config); err != nil {
log.Fatal(errors.Wrap(err, "config: validate"))
}

return
}

Configure OAuth 2.0/OIDC Client

Next we need an OAuth 2.0/OpenID Connect client. Instead of building one from scratch, we use the golang.org/x/oauth2 and github.com/coreos/go-oidc/v3/oidc libraries to construct a custom client type that holds the necessary OAuth 2.0 and OpenID Connect components:

auth/oidc.go
package auth

import (
"context"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/teamhanko/identity-example-go/config"
"golang.org/x/oauth2"
)

// OidcClient holds the necessary OAuth2/OIDC components for authentication
type OidcClient struct {
*oidc.Provider
oauth2.Config
}

// NewOidcClient instantiates an OidcClient.
func NewOidcClient(config config.Config) (*OidcClient, error) {
// Uses OIDC Discovery to determine OIDC provider data (i.e. endpoint URLs)
// See also: https://openid.net/specs/openid-connect-discovery-1_0.html
provider, err := oidc.NewProvider(
context.Background(),
config.ProviderURL,
)
if err != nil {
return nil, err
}

conf := oauth2.Config{
ClientID: config.ClientId,
ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURL,
Endpoint: provider.Endpoint(),
// The "openid" scope is mandatory for all OpenID Connect requests.
// The "offline_access" scope is optional and allows requesting refresh tokens.
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess},
}

return &OidcClient{
Provider: provider,
Config: conf,
}, nil
}

Setting up your application routes

We then set up routing for the application. The example app uses github.com/gin-gonic/gin as its web framework and additionally uses github.com/gin-contrib/sessions for managing application sessions backed by an in-memory store to hold Oauth 2.0/OpenID Connect token data.

router/router.go
package router

import (
"encoding/gob"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/teamhanko/identity-example-go/auth"
"github.com/teamhanko/identity-example-go/config"
"github.com/teamhanko/identity-example-go/handler"
"github.com/teamhanko/identity-example-go/middleware"
)

// New registers the routes and returns the router.
func New(appConfig *config.Config, oidcClient *auth.OidcClient) *gin.Engine {
router := gin.Default()

// Allows using custom type to store token data in session
gob.Register(auth.Token{})

// Use an in memory store to hold token data in the session
store := memstore.NewStore([]byte("secret"))
router.Use(sessions.Sessions("app-session", store))

// Register templates and static assets
router.Static("/public", "static/")
router.LoadHTMLGlob("templates/*")

// Set up main application routes
router.GET("/", handler.Home)
router.GET("/login", handler.Login(oidcClient))
router.GET("/login/callback", handler.LoginCallback(oidcClient))
router.GET("/user", middleware.IsAuthenticated, handler.User(appConfig, oidcClient))
router.GET("/logout", middleware.IsAuthenticated, handler.Logout(oidcClient))

return router
}

Serving your application

In the main application entry point, load the configuration, instantiate our OpenID Connect client and router and serve the app using Go's built-in net/http package:

main.go
package main

import (
"log"
"net/http"

"github.com/teamhanko/identity-example-go/auth"
"github.com/teamhanko/identity-example-go/config"
"github.com/teamhanko/identity-example-go/router"
)

func main() {
// Load configuration
appConfig := config.Load(".")

// Instantiate OIDC client
oidcClient, err := auth.NewOidcClient(appConfig)
if err != nil {
log.Fatalf("failed to initialize OIDC client: %v", err)
}

// Instantiate router, pass configuration and OIDC client so that handlers
// can consume them
appRouter := router.New(&appConfig, oidcClient)

log.Print("server listening on http://localhost:8080")
if err := http.ListenAndServe("0.0.0.0:8080", appRouter); err != nil {
log.Fatalf("there was an error with the http server: %v", err)
}
}

Initializing login

Our sample application serves up a simple home page that provides nothing but a link to our login route (cf. the index.html in the templates folder). The handler for this route initiates the login by redirecting the browser to the Hanko Identity OpenID Connect authorization endpoint. The user will then be redirected to your Hanko Identity tenant's login page to authenticate.

handler/login.go
package handler

import (
"net/http"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/teamhanko/identity-example-go/auth"
"github.com/teamhanko/identity-example-go/util"
)

// Login initiates login based on the OIDC Authorization Code Flow
// See also: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps
func Login(c *auth.OidcClient) gin.HandlerFunc {
return func(ctx *gin.Context) {

// Generate login state. Serves as CSRF attack protection.
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
state, err := util.GenerateState()
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// Save the login state inside a session.
session := sessions.Default(ctx)
session.Set("state", state)
if err := session.Save(); err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// Redirect to authorization endpoint including the login state
// in order to retrieve an authorization code.
ctx.Redirect(http.StatusTemporaryRedirect, c.AuthCodeURL(state))
}
}

Login Callback

After authentication via Hanko Identity's login page, the browser is redirected to the previously configured login redirect callback URL. The handler for this route is responsible for exchanging an authorization code for an ID token and OAuth2 Access/Refresh tokens from the token endpoint:

handler/login.go
// LoginCallback handles the authorization request response containing the authorization code and exchanges
// it for Access/ID/Refresh token with the OIDC Token endpoint. The URL for this handler must be registered
// as a valid redirect URL in the Hanko Console.
func LoginCallback(c *auth.OidcClient) gin.HandlerFunc {
return func(ctx *gin.Context) {
// Get login state from session and verify against state given in the request
session := sessions.Default(ctx)
if ctx.Query("state") != session.Get("state") {
ctx.String(http.StatusBadRequest, "Invalid state parameter.")
return
}

// Clear login state
session.Delete("state")
if err := session.Save(); err != nil {
ctx.String(http.StatusInternalServerError, "Failed to save session.")
return
}

// Exchange an authorization code for access/refresh/id token
// See also: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
token, err := c.Exchange(ctx.Request.Context(), ctx.Query("code"))
if err != nil {
ctx.String(http.StatusUnauthorized, "Failed to convert an authorization code into a token.")
return
}

// Extract ID token from Oauth2 token
idTokenRaw, ok := token.Extra("id_token").(string)
if !ok {
ctx.String(http.StatusInternalServerError, "Failed to extract ID Token.")
return
}

// Verify the ID token
// See also: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
idToken, err := c.Verifier(&oidc.Config{ClientID: c.ClientID}).Verify(ctx, idTokenRaw)
if err != nil {
ctx.String(http.StatusInternalServerError, "Failed to verify ID Token.")
return
}

// Store token data in session
session.Set("token", auth.Token{Oauth2Token: *token, IDToken: *idToken, IDTokenRaw: idTokenRaw})
if err := session.Save(); err != nil {
ctx.String(http.StatusInternalServerError, "Failed to save session.")
return
}

// Redirect to user page on success
ctx.Redirect(http.StatusTemporaryRedirect, "/user")
}
}

Displaying User Profile

To obtain and display user information we can query the OpenID Connect UserInfo endpoint:

package handler

import (
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/teamhanko/identity-example-go/auth"
"github.com/teamhanko/identity-example-go/config"
"github.com/teamhanko/identity-example-go/model"
)

// User provides a handler for rendering user data by querying the OIDC UserInfo endpoint
// See also: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
func User(appConfig *config.Config, c *auth.OidcClient) gin.HandlerFunc {
return func(ctx *gin.Context) {
// Get token data from session
session := sessions.Default(ctx)
token := session.Get("token").(auth.Token)

// Create a token source that automatically refreshes token on expiry
tokenSource := c.Config.TokenSource(ctx, &token.Oauth2Token)

// Query UserInfo endpoint
userInfo, err := c.Provider.UserInfo(ctx, tokenSource)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// Construct user model from returned claims
user, _ := model.UserFromUserInfo(userInfo)

// Construct link to account page including return URL that allows you to return to the
// user page from the Identity profile
accountPageUrl, _ := url.Parse(appConfig.ProviderURL + "account")
query := accountPageUrl.Query()
query.Set("back_url", "http://localhost:8080/user")
accountPageUrl.RawQuery = query.Encode()

// Render data in template
ctx.HTML(http.StatusOK, "user.html", gin.H{
"AccountPageURL": accountPageUrl,
"User": user,
})
}
}

Logging Out

Log out by clearing your application session and redirecting the user to the end session/logout endpoint to log out the user from Hanko Identity:

handler/logout.go
package handler

import (
"net/http"
"net/url"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/teamhanko/identity-example-go/auth"
)

// Logout handles an 'RP-initiated logout'
// See also: https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
func Logout(c *auth.OidcClient) gin.HandlerFunc {
return func(ctx *gin.Context) {
// Get the endpoint for ending a session with the OIDC provider from our OIDC client
var claims struct {
EndSessionURL string `json:"end_session_endpoint"`
}

if err := c.Provider.Claims(&claims); err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

logoutURL, err := url.Parse(claims.EndSessionURL)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// Construct post logout redirect URL
scheme := "http"
if ctx.Request.TLS != nil {
scheme = "https"
}

returnTo, err := url.Parse(scheme + "://" + ctx.Request.Host)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

session := sessions.Default(ctx)
token := session.Get("token").(auth.Token)

logoutURLQuery := logoutURL.Query()

// Indicates the identity of the End-User that the your application is requesting to log out.
logoutURLQuery.Add("id_token_hint", token.IDTokenRaw)

// The post logout redirect URL must have been previously registered with Hanko Identity through your
// application settings in the Hanko Console
logoutURLQuery.Add("post_logout_redirect_uri", returnTo.String())

logoutURL.RawQuery = logoutURLQuery.Encode()

// Clear session data
session.Clear()
if err := session.Save(); err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

ctx.Redirect(http.StatusTemporaryRedirect, logoutURL.String())
}
}