From 4a1889c20bbeda8f1b5fb489032879a0058ae469 Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 26 Feb 2026 16:28:58 +0100 Subject: [PATCH] feat: oidc client create command (#672) * feat: add oidc client create command * refactor: use own utility for creating random strings (more flexible than stdlib) * feat: validate client name to avoid config errors * refactor: limit to only alphanumeric characters and hyphens * refactor: remove the need of the logger in the create oidc client cmd --- cmd/tinyauth/create_oidc_client.go | 43 +++++++++++++++++++ cmd/tinyauth/{create.go => create_user.go} | 0 .../{generate.go => generate_totp.go} | 0 cmd/tinyauth/tinyauth.go | 23 ++++++++-- cmd/tinyauth/{verify.go => verify_user.go} | 2 +- cmd/tinyauth/version.go | 2 +- internal/controller/oidc_controller.go | 3 +- internal/service/oidc_service.go | 8 ++-- internal/utils/security_utils.go | 7 +++ 9 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 cmd/tinyauth/create_oidc_client.go rename cmd/tinyauth/{create.go => create_user.go} (100%) rename cmd/tinyauth/{generate.go => generate_totp.go} (100%) rename cmd/tinyauth/{verify.go => verify_user.go} (98%) diff --git a/cmd/tinyauth/create_oidc_client.go b/cmd/tinyauth/create_oidc_client.go new file mode 100644 index 0000000..862119e --- /dev/null +++ b/cmd/tinyauth/create_oidc_client.go @@ -0,0 +1,43 @@ +package main + +import ( + "errors" + "fmt" + "regexp" + + "github.com/google/uuid" + "github.com/steveiliop56/tinyauth/internal/utils" + "github.com/traefik/paerser/cli" +) + +func createOidcClientCmd() *cli.Command { + return &cli.Command{ + Name: "create", + Description: "Create a new OIDC Client", + Configuration: nil, + Resources: nil, + AllowArg: true, + Run: func(args []string) error { + if len(args) == 0 { + return errors.New("client name is required. use tinyauth oidc create ") + } + + clientName := args[0] + + match, err := regexp.MatchString("^[a-zA-Z0-9-]*$", clientName) + + if !match || err != nil { + return errors.New("client name can only contain alphanumeric characters and hyphens") + } + + uuid := uuid.New() + clientId := uuid.String() + clientSecret := "ta-" + utils.GenerateString(61) + + fmt.Printf("Client Name: %s\n", clientName) + fmt.Printf("Client ID: %s\n", clientId) + fmt.Printf("Client Secret: %s\n", clientSecret) + return nil + }, + } +} diff --git a/cmd/tinyauth/create.go b/cmd/tinyauth/create_user.go similarity index 100% rename from cmd/tinyauth/create.go rename to cmd/tinyauth/create_user.go diff --git a/cmd/tinyauth/generate.go b/cmd/tinyauth/generate_totp.go similarity index 100% rename from cmd/tinyauth/generate.go rename to cmd/tinyauth/generate_totp.go diff --git a/cmd/tinyauth/tinyauth.go b/cmd/tinyauth/tinyauth.go index a6cb93e..0f2825e 100644 --- a/cmd/tinyauth/tinyauth.go +++ b/cmd/tinyauth/tinyauth.go @@ -23,7 +23,7 @@ func main() { cmdTinyauth := &cli.Command{ Name: "tinyauth", - Description: "The simplest way to protect your apps with a login screen.", + Description: "The simplest way to protect your apps with a login screen", Configuration: tConfig, Resources: loaders, Run: func(_ []string) error { @@ -33,12 +33,17 @@ func main() { cmdUser := &cli.Command{ Name: "user", - Description: "Utilities for creating and verifying Tinyauth users.", + Description: "Manage Tinyauth users", } cmdTotp := &cli.Command{ Name: "totp", - Description: "Utilities for creating Tinyauth TOTP users.", + Description: "Manage Tinyauth TOTP users", + } + + cmdOidc := &cli.Command{ + Name: "oidc", + Description: "Manage Tinyauth OIDC clients", } err := cmdTinyauth.AddCommand(versionCmd()) @@ -71,6 +76,12 @@ func main() { log.Fatal().Err(err).Msg("Failed to add create command") } + err = cmdOidc.AddCommand(createOidcClientCmd()) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to add create command") + } + err = cmdTinyauth.AddCommand(cmdUser) if err != nil { @@ -83,6 +94,12 @@ func main() { log.Fatal().Err(err).Msg("Failed to add totp command") } + err = cmdTinyauth.AddCommand(cmdOidc) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to add oidc command") + } + err = cli.Execute(cmdTinyauth) if err != nil { diff --git a/cmd/tinyauth/verify.go b/cmd/tinyauth/verify_user.go similarity index 98% rename from cmd/tinyauth/verify.go rename to cmd/tinyauth/verify_user.go index aa98cbd..7501041 100644 --- a/cmd/tinyauth/verify.go +++ b/cmd/tinyauth/verify_user.go @@ -40,7 +40,7 @@ func verifyUserCmd() *cli.Command { return &cli.Command{ Name: "verify", - Description: "Verify a user is set up correctly.", + Description: "Verify a user is set up correctly", Configuration: tCfg, Resources: loaders, Run: func(_ []string) error { diff --git a/cmd/tinyauth/version.go b/cmd/tinyauth/version.go index aad6b55..b394880 100644 --- a/cmd/tinyauth/version.go +++ b/cmd/tinyauth/version.go @@ -11,7 +11,7 @@ import ( func versionCmd() *cli.Command { return &cli.Command{ Name: "version", - Description: "Print the version number of Tinyauth.", + Description: "Print the version number of Tinyauth", Configuration: nil, Resources: nil, Run: func(_ []string) error { diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 3be8d66..f912062 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -1,7 +1,6 @@ package controller import ( - "crypto/rand" "errors" "fmt" "net/http" @@ -145,7 +144,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) { // WARNING: Since Tinyauth is stateless, we cannot have a sub that never changes. We will just create a uuid out of the username and client name which remains stable, but if username or client name changes then sub changes too. sub := utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.Username, client.ID)) - code := rand.Text() + code := utils.GenerateString(32) // Before storing the code, delete old session err = controller.oidc.DeleteOldSession(c, sub) diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index 2a05dba..5ce5ddd 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -403,8 +403,8 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OI return TokenResponse{}, err } - accessToken := rand.Text() - refreshToken := rand.Text() + accessToken := utils.GenerateString(32) + refreshToken := utils.GenerateString(32) tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix() @@ -464,8 +464,8 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri return TokenResponse{}, err } - accessToken := rand.Text() - newRefreshToken := rand.Text() + accessToken := utils.GenerateString(32) + newRefreshToken := utils.GenerateString(32) tokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix() refrshTokenExpiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry*2) * time.Second).Unix() diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index 40fe713..1b8d8e9 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -1,6 +1,7 @@ package utils import ( + "crypto/rand" "encoding/base64" "errors" "net" @@ -105,3 +106,9 @@ func GenerateUUID(str string) string { uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) return uuid.String() } + +func GenerateString(length int) string { + src := make([]byte, length) + rand.Read(src) + return base64.RawURLEncoding.EncodeToString(src)[:length] +}