singpass

package module
v1.1.4 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2025 License: MIT Imports: 19 Imported by: 0

README

Go Singpass

A comprehensive Go library for integrating with Singapore's Singpass OpenID Connect (OIDC) authentication system.

Features

  • Complete OIDC Integration: Full support for Singpass OpenID Connect authentication flow
  • PKCE Support: Implements Proof Key for Code Exchange for enhanced security
  • JWT Token Handling: Secure JWT token parsing, validation, and user info extraction
  • Redis State Management: Robust state and nonce management using Redis
  • Environment Support: Built-in support for sandbox and production environments
  • Comprehensive User Data: Extract complete user information including personal details, address, and contact info
  • Type Safety: Strongly typed data structures matching Singpass API responses
  • Configurable: Flexible configuration with sensible defaults

Installation

go get github.com/vector233/go-singpass

Quick Start

Basic Setup
package main

import (
    "context"
    "fmt"
    "log"
    
    "github.com/vector233/go-singpass"
)

func main() {
    // Create configuration
    config := &singpass.Config{
        ClientID:    "your-client-id",
        RedirectURI: "https://your-app.com/callback",
        Environment: singpass.EnvironmentSandbox, // or EnvironmentProduction
        RedisAddr:   "localhost:6379",
        RedisDB:     0,
    }
    
    // Initialize client
    client, err := singpass.NewClient(config)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()
    
    // Generate authentication URL
    authURL, err := client.GenerateAuthURL(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Redirect user to: %s\n", authURL)
}
Handle Callback
func handleCallback(client *singpass.Client, code, state string) {
    ctx := context.Background()
    
    // Exchange code for user information
    userInfo, err := client.HandleCallback(ctx, code, state)
    if err != nil {
        log.Printf("Callback error: %v", err)
        return
    }
    
    // Access user information
    fmt.Printf("User: %s\n", userInfo.GetName())
    fmt.Printf("NRIC: %s\n", userInfo.GetUINFIN())
    fmt.Printf("Address: %s\n", userInfo.GetAddress())
}

Configuration

Environment-Specific Configurations
// Sandbox configuration
config := singpass.SandboxConfig()
config.ClientID = "your-sandbox-client-id"
config.RedirectURI = "https://your-app.com/callback"
config.RedisAddr = "localhost:6379"

// Production configuration
config := singpass.ProductionConfig()
config.ClientID = "your-production-client-id"
config.RedirectURI = "https://your-app.com/callback"
config.RedisAddr = "localhost:6379"
Manual Configuration
config := &singpass.Config{
    // OAuth2 Configuration
    ClientID:    "your-client-id",
    RedirectURI: "https://your-app.com/callback",
    Scope:       "openid profile",
    
    // Singpass Endpoints
    AuthURL:     "https://stg-id.singpass.gov.sg/auth",
    TokenURL:    "https://stg-id.singpass.gov.sg/token",
    UserInfoURL: "https://stg-id.singpass.gov.sg/userinfo",
    JWKSURL:     "https://stg-id.singpass.gov.sg/.well-known/jwks",
    
    // Cryptographic Keys
    SigPrivateKeyPath: "/path/to/signing-key.pem",
    EncPrivateKeyPath: "/path/to/encryption-key.pem",
    
    // Redis Configuration
    RedisAddr:     "localhost:6379",
    RedisPassword: "",
    RedisDB:       0,
    
    // Timeouts and Expiration
    StateExpiration: 10 * time.Minute,
    NonceExpiration: 10 * time.Minute,
    JWKSCacheTTL:    24 * time.Hour,
    HTTPTimeout:     30 * time.Second,
}

User Information

The library provides comprehensive access to Singpass user data:

// Personal Information
name := userInfo.GetName()           // Full name
uinfin := userInfo.GetUINFIN()       // NRIC/FIN
sex := userInfo.Sex.Code             // Gender code
dob := userInfo.DOB.Value            // Date of birth
nationality := userInfo.Nationality.Code // Nationality code

// Contact Information
mobile := userInfo.MobileNo.Number.Value // Mobile number
email := userInfo.Email.Value             // Email address

// Address Information
address := userInfo.GetAddress()     // Formatted address string
block := userInfo.RegAdd.Block.Value // HDB block number
unit := userInfo.RegAdd.Unit.Value   // Unit number
postal := userInfo.RegAdd.Postal.Value // Postal code

// JWT Claims
issuer := userInfo.Iss    // Token issuer
subject := userInfo.Sub   // Subject (user ID)
audience := userInfo.Aud  // Intended audience
issuedAt := userInfo.Iat  // Issued at timestamp
expiry := userInfo.Exp    // Expiration timestamp

Error Handling

userInfo, err := client.HandleCallback(ctx, code, state)
if err != nil {
    switch {
    case strings.Contains(err.Error(), "invalid state"):
        // Handle invalid state parameter
    case strings.Contains(err.Error(), "token validation failed"):
        // Handle token validation errors
    case strings.Contains(err.Error(), "JWKS fetch failed"):
        // Handle JWKS retrieval errors
    default:
        // Handle other errors
    }
}

Testing

Run the test suite:

go test -v

The library includes comprehensive tests for:

  • Configuration validation
  • PKCE code generation
  • State management
  • User info parsing
  • Environment-specific configurations

Requirements

  • Go 1.23 or later
  • Redis server for state management
  • Valid Singpass client credentials
  • Private keys for JWT signing and encryption

Dependencies

  • github.com/lestrrat-go/jwx/v2 - JWT handling
  • github.com/redis/go-redis/v9 - Redis client
  • github.com/google/uuid - UUID generation

Security Considerations

  • Store private keys securely and never commit them to version control
  • Use HTTPS for all redirect URIs in production
  • Implement proper session management in your application
  • Validate all user inputs and sanitize data before storage
  • Use appropriate Redis security configurations

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

For issues and questions:

  • Create an issue on GitHub
  • Check the Singpass developer documentation
  • Review the test cases for usage examples

Documentation

Overview

Package singpass provides a Go client library for Singapore's Singpass OpenID Connect (OIDC) authentication. It supports PKCE (Proof Key for Code Exchange) for secure authentication flows and includes comprehensive JWT/JWE token validation with JWKS (JSON Web Key Set) support.

Package singpass provides configuration management for Singpass OIDC authentication.

Package singpass defines error types for Singpass authentication operations.

Package singpass defines data models for Singpass authentication and user information.

Index

Constants

View Source
const (
	StateKeyPrefix = "singpass:state:"
	NonceKeyPrefix = "singpass:nonce:"
)

Constants for Redis key prefixes and cache intervals

View Source
const (
	DefaultScope = "openid profile"

	// Environment constants
	EnvironmentSandbox    = "sandbox"
	EnvironmentProduction = "production"

	// Singpass URLs
	SandboxAuthURL     = "https://stg-id.singpass.gov.sg/auth"
	SandboxTokenURL    = "https://stg-id.singpass.gov.sg/token" // #nosec G101 -- This is a public URL, not a credential
	SandboxUserInfoURL = "https://stg-id.singpass.gov.sg/userinfo"
	SandboxJWKSURL     = "https://stg-id.singpass.gov.sg/.well-known/keys"

	ProductionAuthURL     = "https://id.singpass.gov.sg/auth"
	ProductionTokenURL    = "https://id.singpass.gov.sg/token" // #nosec G101 -- This is a public URL, not a credential
	ProductionUserInfoURL = "https://id.singpass.gov.sg/userinfo"
	ProductionJWKSURL     = "https://id.singpass.gov.sg/.well-known/keys"
)

Default configuration values

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthState

type AuthState struct {
	State         string    `json:"state"`
	Nonce         string    `json:"nonce"`
	CodeVerifier  string    `json:"code_verifier"`
	CodeChallenge string    `json:"code_challenge"`
	CreatedAt     time.Time `json:"created_at"`
	ExpiresAt     time.Time `json:"expires_at"`
}

AuthState represents the state stored during OAuth2 flow

func (*AuthState) IsExpired

func (s *AuthState) IsExpired() bool

IsExpired checks if the auth state is expired

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client represents the Singpass authentication client

func NewClient

func NewClient(config *Config) (*Client, error)

NewClient creates a new Singpass client with the given configuration

func (*Client) Close

func (c *Client) Close() error

Close closes the client and cleans up resources

func (*Client) GenerateAuthURL

func (c *Client) GenerateAuthURL(ctx context.Context) (string, error)

GenerateAuthURL generates the authorization URL for Singpass login

func (*Client) GetUserInfo added in v1.0.2

func (c *Client) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error)

GetUserInfo retrieves user information using access token

func (*Client) HandleCallback

func (c *Client) HandleCallback(ctx context.Context, code, state string) (*UserInfo, error)

HandleCallback handles the OAuth2 callback and returns user information

type CodeDesc added in v1.0.2

type CodeDesc struct {
	Code string `json:"code"`
	Desc string `json:"desc"`
}

CodeDesc represents a code-description pair

type CodedField added in v1.0.2

type CodedField struct {
	LastUpdated    string `json:"lastupdated"`
	Source         string `json:"source"`
	Classification string `json:"classification"`
	Code           string `json:"code"`
	Desc           string `json:"desc"`
}

CodedField represents a Singpass field with code and description

type Config

type Config struct {
	// OAuth2/OIDC Configuration
	ClientID    string `json:"client_id"`
	Scope       string `json:"scope"`
	Issuer      string `json:"issuer"`
	RedirectURI string `json:"redirect_uri"`
	AuthURL     string `json:"auth_url"`
	TokenURL    string `json:"token_url"`
	UserInfoURL string `json:"userinfo_url"`
	JWKSURL     string `json:"jwks_url"`

	// Cryptographic Keys
	SigPrivateKeyPath string `json:"sig_private_key_path,omitempty"`
	EncPrivateKeyPath string `json:"enc_private_key_path,omitempty"`

	// Redis Configuration for state management
	RedisAddr     string `json:"redis_addr"`
	RedisPassword string `json:"redis_password,omitempty"`
	RedisDB       int    `json:"redis_db"`

	// Timeouts and Expiration
	StateExpiration time.Duration `json:"state_expiration,omitempty"`
	NonceExpiration time.Duration `json:"nonce_expiration,omitempty"`
	JWKSCacheTTL    time.Duration `json:"jwks_cache_ttl,omitempty"`
	HTTPTimeout     time.Duration `json:"http_timeout,omitempty"`

	// Environment
	Environment string `json:"environment,omitempty"`
}

Config holds the configuration for Singpass authentication

func DefaultConfig added in v1.0.2

func DefaultConfig() *Config

DefaultConfig returns a default configuration

func ProductionConfig added in v1.0.2

func ProductionConfig() *Config

ProductionConfig returns a configuration for production environment

func SandboxConfig added in v1.0.2

func SandboxConfig() *Config

SandboxConfig returns a configuration for sandbox environment

func (*Config) GetRedisKeyPrefix added in v1.0.2

func (c *Config) GetRedisKeyPrefix() string

GetRedisKeyPrefix returns the Redis key prefix based on environment

func (*Config) IsProduction added in v1.0.2

func (c *Config) IsProduction() bool

IsProduction returns true if the configuration is for production environment

func (*Config) IsSandbox added in v1.0.2

func (c *Config) IsSandbox() bool

IsSandbox returns true if the configuration is for sandbox environment

func (*Config) SetDefaults

func (c *Config) SetDefaults()

SetDefaults sets default values for optional configuration fields

func (*Config) Validate

func (c *Config) Validate() error

Validate checks if the configuration is valid

type ErrHTTPRequest

type ErrHTTPRequest struct {
	StatusCode int
	Message    string
}

ErrHTTPRequest represents an HTTP request error

func (ErrHTTPRequest) Error

func (e ErrHTTPRequest) Error() string

type ErrInvalidConfig

type ErrInvalidConfig struct {
	Field string
}

ErrInvalidConfig represents a configuration validation error

func (ErrInvalidConfig) Error

func (e ErrInvalidConfig) Error() string

type ErrInvalidState

type ErrInvalidState struct {
	Message string
}

ErrInvalidState represents an invalid state parameter error

func (ErrInvalidState) Error

func (e ErrInvalidState) Error() string

type ErrJWKSFetch

type ErrJWKSFetch struct {
	Message string
}

ErrJWKSFetch represents a JWKS fetching error

func (ErrJWKSFetch) Error

func (e ErrJWKSFetch) Error() string

type ErrRedisOperation

type ErrRedisOperation struct {
	Operation string
	Message   string
}

ErrRedisOperation represents a Redis operation error

func (ErrRedisOperation) Error

func (e ErrRedisOperation) Error() string

type ErrTokenValidation

type ErrTokenValidation struct {
	Message string
}

ErrTokenValidation represents a token validation error

func (ErrTokenValidation) Error

func (e ErrTokenValidation) Error() string

type PhoneField added in v1.0.2

type PhoneField struct {
	LastUpdated    string       `json:"lastupdated"`
	Source         string       `json:"source"`
	Classification string       `json:"classification"`
	AreaCode       ValueWrapper `json:"areacode"`
	Prefix         ValueWrapper `json:"prefix"`
	Number         ValueWrapper `json:"nbr"`
}

PhoneField represents a Singpass phone number field

type RegisteredAddress added in v1.0.2

type RegisteredAddress struct {
	LastUpdated    string       `json:"lastupdated"`
	Source         string       `json:"source"`
	Classification string       `json:"classification"`
	Country        CodeDesc     `json:"country"`
	Unit           ValueWrapper `json:"unit"`
	Street         ValueWrapper `json:"street"`
	Block          ValueWrapper `json:"block"`
	Postal         ValueWrapper `json:"postal"`
	Floor          ValueWrapper `json:"floor"`
	Building       ValueWrapper `json:"building"`
	Type           string       `json:"type"`
}

RegisteredAddress represents a Singpass registered address

func (*RegisteredAddress) String added in v1.0.2

func (r *RegisteredAddress) String() string

String returns a formatted address string for RegisteredAddress

type StateData added in v1.0.2

type StateData struct {
	CodeVerifier string `json:"code_verifier"`
	Nonce        string `json:"nonce"`
}

StateData represents OAuth state information

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in,omitempty"`
	RefreshToken string `json:"refresh_token,omitempty"`
	IDToken      string `json:"id_token"`
	Scope        string `json:"scope,omitempty"`
}

TokenResponse represents the OAuth2 token response

type UserInfo

type UserInfo struct {
	// Personal Information (Singpass format)
	Name        ValueField `json:"name"`
	UINFIN      ValueField `json:"uinfin"`
	Sex         CodedField `json:"sex"`
	DOB         ValueField `json:"dob"`
	Nationality CodedField `json:"nationality"`

	// Address Information
	RegAdd RegisteredAddress `json:"regadd"`

	// Contact Information
	MobileNo PhoneField `json:"mobileno"`
	Email    ValueField `json:"email"`

	// Housing Information
	Housingtype CodedField `json:"housingtype"`

	// JWT Claims
	Iss string `json:"iss"`
	Sub string `json:"sub"`
	Aud string `json:"aud"`
	Iat int64  `json:"iat"`
	Exp int64  `json:"exp,omitempty"`
}

UserInfo represents the user information returned by Singpass This is the complete structure as returned by Singpass UserInfo endpoint

func (*UserInfo) GetAddress added in v1.0.2

func (u *UserInfo) GetAddress() string

GetAddress returns the formatted address string

func (*UserInfo) GetName added in v1.0.2

func (u *UserInfo) GetName() string

GetName returns the user's full name

func (*UserInfo) GetUINFIN added in v1.0.2

func (u *UserInfo) GetUINFIN() string

GetUINFIN returns the user's UINFIN (unique identification number)

func (*UserInfo) IsExpired added in v1.0.2

func (u *UserInfo) IsExpired() bool

IsExpired checks if the user info has expired

type ValueField added in v1.0.2

type ValueField struct {
	LastUpdated    string `json:"lastupdated"`
	Source         string `json:"source"`
	Classification string `json:"classification"`
	Value          string `json:"value"`
}

ValueField represents a Singpass field with metadata

type ValueWrapper added in v1.0.2

type ValueWrapper struct {
	Value string `json:"value"`
}

ValueWrapper wraps a simple value

Jump to

Keyboard shortcuts

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