Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/.well-known/jwks.json": {
"get": {
"description": "Returns the public keys used to verify JWTs issued by this server",
"produces": [
"application/json"
],
"tags": [
"OpenID Connect"
],
"summary": "Get JSON Web Key Set",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/github_com_roshankumar0036singh_auth-server_internal_dto.JWKSResponse"
}
}
}
}
},
"/api/admin/users": {
"get": {
"security": [
Expand Down Expand Up @@ -1405,6 +1425,46 @@ const docTemplate = `{
}
}
},
"github_com_roshankumar0036singh_auth-server_internal_dto.JWK": {
"type": "object",
"properties": {
"alg": {
"description": "Algorithm",
"type": "string"
},
"e": {
"description": "Exponent (Base64url encoded)",
"type": "string"
},
"kid": {
"description": "Key ID",
"type": "string"
},
"kty": {
"description": "Key Type",
"type": "string"
},
"n": {
"description": "Modulus (Base64url encoded)",
"type": "string"
},
"use": {
"description": "Public Key Use (e.g., \"sig\")",
"type": "string"
}
}
},
"github_com_roshankumar0036singh_auth-server_internal_dto.JWKSResponse": {
"type": "object",
"properties": {
"keys": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_roshankumar0036singh_auth-server_internal_dto.JWK"
}
}
}
},
"github_com_roshankumar0036singh_auth-server_internal_dto.LoginRequest": {
"type": "object",
"required": [
Expand Down
60 changes: 60 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@
"host": "localhost:8080",
"basePath": "/",
"paths": {
"/.well-known/jwks.json": {
"get": {
"description": "Returns the public keys used to verify JWTs issued by this server",
"produces": [
"application/json"
],
"tags": [
"OpenID Connect"
],
"summary": "Get JSON Web Key Set",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/github_com_roshankumar0036singh_auth-server_internal_dto.JWKSResponse"
}
}
}
}
},
"/api/admin/users": {
"get": {
"security": [
Expand Down Expand Up @@ -1403,6 +1423,46 @@
}
}
},
"github_com_roshankumar0036singh_auth-server_internal_dto.JWK": {
"type": "object",
"properties": {
"alg": {
"description": "Algorithm",
"type": "string"
},
"e": {
"description": "Exponent (Base64url encoded)",
"type": "string"
},
"kid": {
"description": "Key ID",
"type": "string"
},
"kty": {
"description": "Key Type",
"type": "string"
},
"n": {
"description": "Modulus (Base64url encoded)",
"type": "string"
},
"use": {
"description": "Public Key Use (e.g., \"sig\")",
"type": "string"
}
}
},
"github_com_roshankumar0036singh_auth-server_internal_dto.JWKSResponse": {
"type": "object",
"properties": {
"keys": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_roshankumar0036singh_auth-server_internal_dto.JWK"
}
}
}
},
"github_com_roshankumar0036singh_auth-server_internal_dto.LoginRequest": {
"type": "object",
"required": [
Expand Down
41 changes: 41 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@ definitions:
required:
- email
type: object
github_com_roshankumar0036singh_auth-server_internal_dto.JWK:
properties:
alg:
description: Algorithm
type: string
e:
description: Exponent (Base64url encoded)
type: string
kid:
description: Key ID
type: string
kty:
description: Key Type
type: string
"n":
description: Modulus (Base64url encoded)
type: string
use:
description: Public Key Use (e.g., "sig")
type: string
type: object
github_com_roshankumar0036singh_auth-server_internal_dto.JWKSResponse:
properties:
keys:
items:
$ref: '#/definitions/github_com_roshankumar0036singh_auth-server_internal_dto.JWK'
type: array
type: object
github_com_roshankumar0036singh_auth-server_internal_dto.LoginRequest:
properties:
email:
Expand Down Expand Up @@ -285,6 +313,19 @@ info:
title: Auth Server API
version: "1.0"
paths:
/.well-known/jwks.json:
get:
description: Returns the public keys used to verify JWTs issued by this server
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/github_com_roshankumar0036singh_auth-server_internal_dto.JWKSResponse'
summary: Get JSON Web Key Set
tags:
- OpenID Connect
/api/admin/users:
get:
produces:
Expand Down
60 changes: 45 additions & 15 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package config

import (
"crypto/rand"
"crypto/rsa"
"log"
"os"
"strconv"

"github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv"
)

Expand Down Expand Up @@ -36,11 +39,12 @@ type RedisConfig struct {
}

type JWTConfig struct {
AccessSecret string
RefreshSecret string
AccessExpiry string
RefreshExpiry string
RefreshGracePeriod string
PrivateKey *rsa.PrivateKey
PublicKey *rsa.PublicKey
KeyID string
AccessExpiry string
RefreshExpiry string
RefreshGracePeriod string
}
type OAuthConfig struct {
Google GoogleOAuthConfig
Expand Down Expand Up @@ -112,14 +116,8 @@ func LoadConfig() *Config {

appURL := getEnv("APP_URL", "http://localhost:3000")

accessSecret := getEnv("JWT_SECRET", "")
refreshSecret := getEnv("JWT_REFRESH_SECRET", "")
if len(accessSecret) < 32 {
log.Fatal("JWT_SECRET must be set and at least 32 bytes long")
}
if len(refreshSecret) < 32 {
log.Fatal("JWT_REFRESH_SECRET must be set and at least 32 bytes long")
}
privKey, pubKey := loadRSAKeys()
keyID := getEnv("JWT_KEY_ID", "default-key-1")

encKey := getEnv("ENCRYPTION_KEY", "")
if encKey == "" || encKey == "0123456789abcdef0123456789abcdef" {
Expand All @@ -142,8 +140,9 @@ func LoadConfig() *Config {
TTL: redisTTL,
},
JWT: JWTConfig{
AccessSecret: getEnv("JWT_SECRET", ""),
RefreshSecret: getEnv("JWT_REFRESH_SECRET", ""),
PrivateKey: privKey,
PublicKey: pubKey,
KeyID: keyID,
AccessExpiry: getEnv("JWT_ACCESS_EXPIRY", "15m"),
RefreshExpiry: getEnv("JWT_REFRESH_EXPIRY", "168h"),
RefreshGracePeriod: getEnv("JWT_REFRESH_GRACE_PERIOD", "10s"),
Expand Down Expand Up @@ -187,3 +186,34 @@ func getEnv(key, defaultValue string) string {
}
return defaultValue
}

func loadRSAKeys() (*rsa.PrivateKey, *rsa.PublicKey) {
privPath := getEnv("JWT_PRIVATE_KEY_PATH", "private.pem")
pubPath := getEnv("JWT_PUBLIC_KEY_PATH", "public.pem")

privBytes, err1 := os.ReadFile(privPath)
pubBytes, err2 := os.ReadFile(pubPath)

if os.IsNotExist(err1) || os.IsNotExist(err2) {
log.Println("RSA keys not found at provided paths, generating temporary in-memory keys for development/testing...")
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatalf("Failed to generate temp RSA key: %v", err)
}
return privKey, &privKey.PublicKey
} else if err1 != nil || err2 != nil {
log.Fatalf("Failed to read RSA keys: privErr=%v, pubErr=%v", err1, err2)
}

privKey, err := jwt.ParseRSAPrivateKeyFromPEM(privBytes)
if err != nil {
log.Fatalf("Failed to parse private key: %v", err)
}

pubKey, err := jwt.ParseRSAPublicKeyFromPEM(pubBytes)
if err != nil {
log.Fatalf("Failed to parse public key: %v", err)
}

return privKey, pubKey
}
16 changes: 16 additions & 0 deletions internal/dto/jwks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dto

// JWK represents a single JSON Web Key
type JWK struct {
Kty string `json:"kty"` // Key Type
Alg string `json:"alg"` // Algorithm
Use string `json:"use"` // Public Key Use (e.g., "sig")
Kid string `json:"kid"` // Key ID
N string `json:"n"` // Modulus (Base64url encoded)
E string `json:"e"` // Exponent (Base64url encoded)
}

// JWKSResponse represents a JSON Web Key Set
type JWKSResponse struct {
Keys []JWK `json:"keys"`
}
15 changes: 15 additions & 0 deletions internal/dto/oidc_discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dto

// OIDCDiscoveryResponse represents the standard OpenID Connect discovery configuration
type OIDCDiscoveryResponse struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
JwksURI string `json:"jwks_uri"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
}
3 changes: 2 additions & 1 deletion internal/handler/auth_handler_protected_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ func TestAuthHandler_GetMe(t *testing.T) {
defer mr.Close()
authHandler := handler.NewAuthHandler(authService, nil, nil)
// We need TokenService to create a valid token for the middleware
cfg := &config.Config{JWT: config.JWTConfig{AccessSecret: "secret"}}
priv, pub := testutils.GetTestRSAKeys(t)
cfg := &config.Config{JWT: config.JWTConfig{PrivateKey: priv, PublicKey: pub, KeyID: "test-key"}}
tokenService := service.NewTokenService(cfg)

rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
Expand Down
6 changes: 4 additions & 2 deletions internal/handler/auth_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,12 @@ func TestAuthHandler_GetSessions_CurrentSessionFlag(t *testing.T) {

authHandler := NewAuthHandler(authService, nil, nil)

priv, pub := testutils.GetTestRSAKeys(t)
cfg := &config.Config{
JWT: config.JWTConfig{
AccessSecret: "secret",
RefreshSecret: "refresh-secret",
PrivateKey: priv,
PublicKey: pub,
KeyID: "test-key",
},
}
tokenService := service.NewTokenService(cfg)
Expand Down
Loading
Loading