Introduction

For teams that have multiple projects to develop, each requiring user authentication and authorization, Zitadel is a good solution. You don’t need to redevelop authentication and authorization logic for each project.

Zitadel supports multi-tenancy and customization of authentication pages with branding. As a backend developer, we only need to integrate the official SDK!

If you’re not familiar with Zitadel, you can check out my article Open Source Identity Authentication and Authorization Solutions.

Concepts

Zitadel issues two types of tokens for logged-in users:

  • Access Token
  • ID Token

Access Tokens are further divided into Bearer Tokens and JSON Web Tokens, with the difference being whether they carry some basic data.

ID Tokens are essentially also a type of JWT, but they contain more comprehensive user information, such as user Metadata, Roles, Permissions, and other data.

Token Verification

Whether it’s an Access Token or an ID Token, to confirm a user’s identity, you must verify that the token carried in the request was issued by the specified Issuer and has not been tampered with.

To achieve this, you need to use an RSA Public Key for signature verification. This logic has a specific term called Introspection, and there are two ways to verify tokens:

  • The resource server requests the identity authentication server’s Introspection Endpoint
  • The resource server caches the identity authentication server’s JWK Set (i.e., the RSA Public Key) and performs verification locally, which is known as: Self-Introspection

However, these two methods have their respective advantages and disadvantages, and you need to decide which one to use based on your specific business requirements.

The method of verifying tokens through the Introspection Endpoint can achieve a high degree of uniformity. When a user logs out or a token is revoked, the resource server can learn that the token has become invalid from the identity authentication server’s response. But the problem with this approach is also quite prominent: each request needs to be verified at the Introspection Endpoint, which can increase API response time by tens to hundreds of milliseconds!

The Self-Introspection method avoids performance issues by caching the identity authentication service’s JWK Set and then verifying the token locally, reducing unnecessary requests and having no impact on response time. However, the problem is also quite prominent: the token cannot be revoked during its validity period!

So, if your business prioritizes security, choose verification through the Introspection Endpoint. If you prioritize performance, choose Self-Introspection!

Project Integration

  • Create a project in Zitadel, and create both Web and API applications for the project:

Zitadel Project Setting

  • Web application settings are as follows:

Zitadel Application Setting Zitadel Token Setting

  • Create a Key under the API application

Zitadel API Key Setting

After creation, you’ll be prompted to download a JSON file for the key. This file is what middleware.OSKeyPath() points to, and you can set the path to this key using the ZITADEL_KEY_PATH environment variable.

Additionally, the framework I’m using is fiber, which is based on fastrouter and differs somewhat from Go’s built-in router!

Introspection

go get github.com/zitadel/zitadel-go/v2
package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/log"
    "github.com/gofiber/fiber/v2/middleware/adaptor"
    http_mw "github.com/zitadel/zitadel-go/v2/pkg/api/middleware/http"
    "github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
)

func profile(ctx *fiber.Ctx) error {
    return ctx.SendString("Success")
}

func main() {
    app := fiber.New()
    
    api := app.Group("/api")

    introspection, err := http_mw.NewIntrospectionInterceptor("https://zitadel.local", middleware.OSKeyPath())
    if err != nil {
        log.Error(err)
    }

    api.Use(adaptor.HTTPMiddleware(introspection.Handler))

    api.Get("/user/profile", profile).Name("User profile")

    log.Fatal(app.Listen(":3000"))
}

Code explanation:

  • Line 20: Declare the interceptor
  • Line 25: Convert the middleware to a fiber Handler

Currently, the official SDK does not yet support retrieving user information. At the application layer, you can only know whether the token is valid. Although there is user information in the response from /oauth/v2/introspect, it is not processed in the SDK!

Self-Introspection

package main

import (
    "net/http"
    "strings"
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/log"
    "github.com/zitadel/oidc/pkg/client/rp"
    "github.com/zitadel/oidc/pkg/oidc"
    "github.com/zitadel/oidc/pkg/op"
)

var (
    keySet   oidc.KeySet
    verifier op.AccessTokenVerifier
)

func init() {
    keySet = rp.NewRemoteKeySet(http.DefaultClient, "https://zitadel.local/oauth/v2/keys")
    verifier = op.NewAccessTokenVerifier("https://zitadel.local", keySet)
}

func authHandler() fiber.Handler {
    return func(ctx *fiber.Ctx) (err error) {
        token := ctx.Get("Authorization")
        token = strings.TrimPrefix(token, oidc.PrefixBearer)

        claims, err := op.VerifyAccessToken(context.Background(), token, verifier)
        if err != nil {
            log.Error(err)
            return ctx.JSON(http.UnAuthenticated("Authentication failed"))
        }

        ctx.Locals("user", claims)

        return ctx.Next()
    }
}

func main() {
    app := fiber.New()
    
    api := app.Group("/api")

    api.Use(authHandler())

    api.Get("/user/profile", profile).Name("User profile")

    log.Fatal(app.Listen(":3000"))
}

Code explanation:

  • Line 19: Declare Remote Key Set, note that this variable is global and stateful, used to cache the Key Set
  • Line 20: Declare the Token verifier
  • Line 28: Verify whether the Token is valid and get Claims
  • Line 34: Put Claims into fiber.Ctx for subsequent logic to retrieve user information

Conclusion

With this, the integration of Zitadel in a Go project is complete. For small projects, adopting this approach can further reduce development cycles and deliver projects earlier!

Going a step further, you could implement user authentication and authorization on an API Gateway or other proxy server, such as Traefik, and the backend can obtain user information through agreed-upon request headers!

I hope this is helpful, Happy hacking…