Skip to content

Commit

Permalink
feat!: enable oidc verification
Browse files Browse the repository at this point in the history
This PR enables tokens to be verified with an OIDC provider. The console service does NOT provide a token from an OIDC provider,
 a token must retrieved via other means such as OAuth 2.0 PKCE Flow in our sample-web-ui.

BREAKING CHANGE: moves JWT configuration to a new "auth" section in the config.yml along.
View config.go for complete example.
  • Loading branch information
rsdmike committed Feb 5, 2025
1 parent d54ab95 commit a60b122
Show file tree
Hide file tree
Showing 14 changed files with 117 additions and 59 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
DISABLE_SWAGGER_HTTP_HANDLER=true
GIN_MODE=release
# DB_URL=postgres://postgresadmin:admin123@localhost:5432/rpsdb
# OAUTH CONFIGURATION
AUTH_CLIENT_ID=""
# ex. "https://login.microsoftonline.com/<tenant-id>/v2.0 for Azure Entra -- used for discovery
AUTH_ISSUER=""
2 changes: 1 addition & 1 deletion .github/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ linters-settings:
misspell:
locale: US
nestif:
min-complexity: 4
min-complexity: 5
nolintlint:
require-explanation: true
require-specific: true
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
.env
.DS_store
*.pem

**/*.yml
**/*.yaml
!config/config.yml
# Binaries for programs and plugins
*.exe
*.exe~
Expand Down
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ linters-settings:
misspell:
locale: US
nestif:
min-complexity: 4
min-complexity: 5
nolintlint:
require-explanation: true
require-specific: true
Expand Down
51 changes: 32 additions & 19 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,15 @@ type (
Log `yaml:"logger"`
DB `yaml:"postgres"`
EA `yaml:"ea"`
Auth `yaml:"auth"`
}

// App -.
App struct {
Name string `env-required:"true" yaml:"name" env:"APP_NAME"`
Repo string `env-required:"true" yaml:"repo" env:"APP_REPO"`
Version string `env-required:"true"`
EncryptionKey string `yaml:"encryption_key" env:"APP_ENCRYPTION_KEY"`
JWTKey string `env-required:"true" yaml:"jwtKey" env:"APP_JWT_KEY"`
AuthDisabled bool `yaml:"authDisabled" env:"APP_AUTH_DISABLED"`
AdminUsername string `yaml:"adminUsername" env:"APP_ADMIN_USERNAME"`
AdminPassword string `yaml:"adminPassword" env:"APP_ADMIN_PASSWORD"`
JWTExpiration time.Duration `yaml:"jwtExpiration" env:"APP_JWT_EXPIRATION"`
RedirectionJWTExpiration time.Duration `yaml:"redirectionJWTExpiration" env:"APP_REDIRECTION_JWT_EXPIRATION"`
Name string `env-required:"true" yaml:"name" env:"APP_NAME"`
Repo string `env-required:"true" yaml:"repo" env:"APP_REPO"`
Version string `env-required:"true"`
EncryptionKey string `yaml:"encryption_key" env:"APP_ENCRYPTION_KEY"`
}

// HTTP -.
Expand Down Expand Up @@ -62,22 +57,30 @@ type (
Username string `yaml:"username" env:"EA_USERNAME"`
Password string `yaml:"password" env:"EA_PASSWORD"`
}

Auth struct {
Disabled bool `yaml:"disabled" env:"AUTH_DISABLED"`
// BASIC
AdminUsername string `yaml:"adminUsername" env:"AUTH_ADMIN_USERNAME"`
AdminPassword string `yaml:"adminPassword" env:"AUTH_ADMIN_PASSWORD"`
JWTKey string `env-required:"true" yaml:"jwtKey" env:"AUTH_JWT_KEY"`
JWTExpiration time.Duration `yaml:"jwtExpiration" env:"AUTH_JWT_EXPIRATION"`
RedirectionJWTExpiration time.Duration `yaml:"redirectionJWTExpiration" env:"AUTH_REDIRECTION_JWT_EXPIRATION"`
// OAUTH
ClientID string `yaml:"clientId" env:"AUTH_CLIENT_ID"`
Issuer string `yaml:"issuer" env:"AUTH_ISSUER"`
}
)

// NewConfig returns app config.
func NewConfig() (*Config, error) {
// set defaults
ConsoleConfig = &Config{
App: App{
Name: "console",
Repo: "open-amt-cloud-toolkit/console",
Version: "DEVELOPMENT",
EncryptionKey: "",
JWTKey: "your_secret_jwt_key",
AdminUsername: "standalone",
AdminPassword: "G@ppm0ym",
JWTExpiration: 24 * time.Hour,
RedirectionJWTExpiration: 5 * time.Minute,
Name: "console",
Repo: "open-amt-cloud-toolkit/console",
Version: "DEVELOPMENT",
EncryptionKey: "",
},
HTTP: HTTP{
Host: "localhost",
Expand All @@ -97,6 +100,16 @@ func NewConfig() (*Config, error) {
Username: "",
Password: "",
},
Auth: Auth{
AdminUsername: "standalone",
AdminPassword: "G@ppm0ym",
JWTKey: "your_secret_jwt_key",
JWTExpiration: 24 * time.Hour,
RedirectionJWTExpiration: 5 * time.Minute,
// OAUTH CONFIG, if provided will not use basic auth
ClientID: "",
Issuer: "",
},
}

// Define a command line flag for the config path
Expand Down
32 changes: 17 additions & 15 deletions config/config.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
app:
name: "console"
repo: "open-amt-cloud-toolkit/console"
jwtKey: "your_secret_jwt_key"
adminUsername: "standalone"
adminPassword: "G@ppm0ym"
jwtExpiration: 24h
redirectionJWTExpiration: 5m
name: console
repo: open-amt-cloud-toolkit/console
version: DEVELOPMENT
encryption_key: ""
http:
host: "localhost"
host: localhost
port: "8181"
allowed_origins:
- "*"
allowed_headers:
- "*"

logger:
log_level: "debug"

log_level: info
postgres:
pool_max: 5
pool_max: 2
url: ""

ea:
url: "http://localhost:8000"
url: http://localhost:8000
username: ""
password: ""

auth:
disabled: false
adminUsername: standalone
adminPassword: G@ppm0ym
jwtKey: your_secret_jwt_key
jwtExpiration: 24h0m0s
redirectionJWTExpiration: 5m0s
clientId: ""
issuer: ""
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ services:
HTTP_HOST: ""
GIN_MODE: "debug"
DB_URL: "postgres://postgresadmin:admin123@postgres:5432/rpsdb"
APP_AUTH_DISABLED: true
AUTH_DISABLED: true
ports:
- 8181:8181
depends_on:
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ toolchain go1.23.1

require (
github.com/Masterminds/squirrel v1.5.4
github.com/coreos/go-oidc/v3 v3.12.0
github.com/gin-contrib/cors v1.7.3
github.com/gin-gonic/gin v1.10.0
github.com/go-xmlfmt/xmlfmt v1.1.3
Expand All @@ -34,12 +35,14 @@ require (
github.com/99designs/keyring v1.2.2 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/term v0.28.0 // indirect
modernc.org/libc v1.61.4 // indirect
)
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
Expand Down Expand Up @@ -58,6 +60,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand Down Expand Up @@ -255,6 +259,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
Expand Down
3 changes: 1 addition & 2 deletions internal/controller/http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg
// Public routes
login := v1.NewLoginRoute(cfg)
handler.POST("/api/v1/authorize", login.Login)

// Static files
// Serve static assets (js, css, images, etc.)
// Create subdirectory view of the embedded file system
Expand Down Expand Up @@ -81,7 +80,7 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg

// Protected routes using JWT middleware
var protected *gin.RouterGroup
if cfg.App.AuthDisabled {
if cfg.Auth.Disabled {
protected = handler.Group("/api")
} else {
protected = handler.Group("/api", login.JWTAuthMiddleware())
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/http/v1/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (dr *deviceRoutes) LoginRedirection(c *gin.Context) {

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

tokenString, err := token.SignedString([]byte(config.ConsoleConfig.App.JWTKey))
tokenString, err := token.SignedString([]byte(config.ConsoleConfig.Auth.JWTKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not create token"})

Expand Down
59 changes: 44 additions & 15 deletions internal/controller/http/v1/login.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package v1

import (
"context"
"net/http"
"strings"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"

Expand All @@ -16,17 +18,31 @@ import (
var ErrLogin = consoleerrors.CreateConsoleError("LoginHandler")

type LoginRoute struct {
Config *config.Config
Config *config.Config
Verifier *oidc.IDTokenVerifier
}

// NewVersionRoute creates a new version route
func NewLoginRoute(configData *config.Config) *LoginRoute {
return &LoginRoute{
lr := &LoginRoute{
Config: configData,
}

if config.ConsoleConfig.ClientID != "" {
provider, err := oidc.NewProvider(context.Background(), config.ConsoleConfig.Issuer)
if err != nil {
return nil
}

lr.Verifier = provider.Verifier(&oidc.Config{
ClientID: config.ConsoleConfig.ClientID,
})
}

return lr
}

// FetchLatestRelease fetches the latest release information from GitHub API
// Login checks configured credentials and returns a JWT token for basic auth
func (lr LoginRoute) Login(c *gin.Context) {
var creds dto.Credentials

Expand All @@ -36,6 +52,10 @@ func (lr LoginRoute) Login(c *gin.Context) {
return
}

lr.handleBasicAuth(creds, c)
}

func (lr LoginRoute) handleBasicAuth(creds dto.Credentials, c *gin.Context) {
if creds.Username != lr.Config.AdminUsername || creds.Password != lr.Config.AdminPassword {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})

Expand All @@ -50,7 +70,7 @@ func (lr LoginRoute) Login(c *gin.Context) {

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

tokenString, err := token.SignedString([]byte(lr.Config.App.JWTKey))
tokenString, err := token.SignedString([]byte(lr.Config.Auth.JWTKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not create token"})

Expand All @@ -73,17 +93,26 @@ func (lr LoginRoute) JWTAuthMiddleware() gin.HandlerFunc {
return
}

claims := &jwt.MapClaims{}

token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) {
return []byte(lr.Config.App.JWTKey), nil
})

if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid access token"})
c.Abort()

return
// if clientID is set, use the oidc verifier
if config.ConsoleConfig.ClientID != "" {
_, err := lr.Verifier.Verify(c.Request.Context(), tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid access token"})
c.Abort()
}
} else {
claims := &jwt.MapClaims{}

token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) {
return []byte(lr.Config.Auth.JWTKey), nil
})

if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid access token"})
c.Abort()

return
}
}

c.Next()
Expand Down
4 changes: 2 additions & 2 deletions internal/controller/ws/v1/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (r *RedirectRoutes) websocketHandler(c *gin.Context) {
tokenString := c.GetHeader("Sec-Websocket-Protocol")

// validate jwt token in the Sec-Websocket-protocol header
if !config.ConsoleConfig.AuthDisabled {
if !config.ConsoleConfig.Disabled {
if tokenString == "" {
http.Error(c.Writer, "request does not contain an access token", http.StatusUnauthorized)

Expand All @@ -41,7 +41,7 @@ func (r *RedirectRoutes) websocketHandler(c *gin.Context) {
claims := &jwt.MapClaims{}

token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) {
return []byte(config.ConsoleConfig.App.JWTKey), nil
return []byte(config.ConsoleConfig.Auth.JWTKey), nil
})

if err != nil || !token.Valid {
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/ws/v1/redirect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestWebSocketHandler(t *testing.T) { //nolint:paralleltest // logging libra

_, _ = config.NewConfig()

config.ConsoleConfig.AuthDisabled = true
config.ConsoleConfig.Disabled = true
mockFeature := mocks.NewMockFeature(ctrl)
mockUpgrader := mocks.NewMockUpgrader(ctrl)
mockLogger := mocks.NewMockLogger(ctrl)
Expand Down

0 comments on commit a60b122

Please sign in to comment.