Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
229
keycloak.go5.38 kB
package keycloak import ( "context" "fmt" "net/mail" "strings" "time" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" "github.com/Southclaws/fault" "github.com/Southclaws/fault/fctx" "github.com/Southclaws/fault/fmsg" "github.com/Southclaws/fault/ftag" "github.com/Southclaws/storyden/app/resources/account" "github.com/Southclaws/storyden/app/resources/account/authentication" "github.com/Southclaws/storyden/app/services/account/register" "github.com/Southclaws/storyden/app/services/authentication/provider/oauth" "github.com/Southclaws/storyden/internal/config" "github.com/Southclaws/storyden/internal/infrastructure/endec" ) var ( service = authentication.ServiceOAuthKeycloak tokenType = authentication.TokenTypeOAuth ) // Provider implements OAuthProvider using Keycloak OIDC discovery. type Provider struct { config oauth.Configuration register *register.Registrar ed endec.EncrypterDecrypter issuer *oidc.Provider verifier *oidc.IDTokenVerifier } // New constructs a Keycloak OAuth provider, running OIDC discovery. func New( cfg config.Config, register *register.Registrar, ed endec.EncrypterDecrypter, ) (*Provider, error) { if !cfg.KeycloakEnabled { return &Provider{ config: oauth.Configuration{ Enabled: cfg.KeycloakEnabled, ClientID: cfg.KeycloakClientID, ClientSecret: cfg.KeycloakClientSecret, }, }, nil } if ed == nil { return nil, fault.New("JWT provider must be enabled by setting JWT_SECRET for Keycloak OAuth provider") } ctx := context.Background() issuer, err := oidc.NewProvider(ctx, cfg.KeycloakIssuerURL.String()) if err != nil { return nil, fault.Wrap(err) } verifier := issuer.Verifier(&oidc.Config{ClientID: cfg.KeycloakClientID}) return &Provider{ config: oauth.Configuration{ Enabled: cfg.KeycloakEnabled, ClientID: cfg.KeycloakClientID, ClientSecret: cfg.KeycloakClientSecret, }, register: register, ed: ed, issuer: issuer, verifier: verifier, }, nil } // Service returns the authentication.Service this provider implements. func (p *Provider) Service() authentication.Service { return service } // Token returns the token type used by this provider (always OAuth). func (p *Provider) Token() authentication.TokenType { return tokenType } // Enabled reports whether the provider is enabled via config. func (p *Provider) Enabled(ctx context.Context) (bool, error) { return p.config.Enabled, nil } // oauthConfig builds the OAuth2 configuration using OIDC discovery. func (p *Provider) oauthConfig(redirect string) *oauth2.Config { return &oauth2.Config{ ClientID: p.config.ClientID, ClientSecret: p.config.ClientSecret, Endpoint: p.issuer.Endpoint(), RedirectURL: redirect, Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } } // Link returns the URL to redirect a user to Keycloak for authentication. func (p *Provider) Link(redirectPath string) (string, error) { state, err := p.ed.Encrypt(map[string]any{"redirect": redirectPath}, time.Minute*10) if err != nil { return "", fault.Wrap(err) } oac := p.oauthConfig(redirectPath) return oac.AuthCodeURL(state, oauth2.AccessTypeOffline), nil } // Login completes the OAuth2 flow: exchanges code, verifies ID token, and returns the Account. func (p *Provider) Login(ctx context.Context, state, code string) (*account.Account, error) { c, err := p.ed.Decrypt(state) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.WithDesc("failed to decrypt state value", "This link has expired, please try again."), ) } redirect := c["redirect"].(string) oac := p.oauthConfig(redirect) tok, err := oac.Exchange(ctx, code) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), ftag.With(ftag.InvalidArgument), fmsg.WithDesc("failed to exchange code for token", "This login token may have expired, please try again."), ) } rawID, ok := tok.Extra("id_token").(string) if !ok { return nil, fault.New("no id_token field in oauth2 token", fctx.With(ctx)) } idToken, err := p.verifier.Verify(ctx, rawID) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.WithDesc("failed to verify Keycloak ID token", "Authentication failed. The login token may be invalid or expired. Please try again.")) } var claims struct { Email string `json:"email"` EmailVerified bool `json:"email_verified"` PreferredUsername string `json:"preferred_username"` Name string `json:"name"` } if err := idToken.Claims(&claims); err != nil { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.WithDesc("failed to parse Keycloak token claims", "Unable to read authentication information. Please try again.")) } handle := strings.ToLower(claims.PreferredUsername) if handle == "" { parts := strings.Split(claims.Email, "@") handle = parts[0] } name := claims.Name emailAddr, err := mail.ParseAddress(claims.Email) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), fmsg.WithDesc("failed to parse Keycloak email address", "The email address from Keycloak is invalid. Please check your account settings.")) } authName := fmt.Sprintf("Keycloak (%s)", emailAddr.Address) return p.register.GetOrCreateViaEmail( ctx, service, authName, idToken.Subject, tok.AccessToken, handle, name, *emailAddr, ) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server