OIDC: Add additional config options and OAuth2 API endpoints #782

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-06-26 14:05:58 +02:00
parent 7af4a1741b
commit c0ea3c79a3
24 changed files with 287 additions and 101 deletions

View File

@@ -45,6 +45,11 @@ services:
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_REGISTER_URI: "https://keycloak.localssl.dev/admin/"
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
## OpenID Connect (pre-configured for local tests):
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/auth/realms/master"
PHOTOPRISM_OIDC_INSECURE: "true"
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
## LDAP Authentication (pre-configured for local tests):
PHOTOPRISM_LDAP_URI: "ldap://dummy-ldap:389"
PHOTOPRISM_LDAP_INSECURE: "true"
@@ -55,11 +60,6 @@ services:
PHOTOPRISM_LDAP_ROLE: ""
PHOTOPRISM_LDAP_ROLE_DN: "ou=photoprism-*,ou=groups,dc=localssl,dc=dev"
PHOTOPRISM_LDAP_WEBDAV_DN: "ou=photoprism-webdav,ou=groups,dc=localssl,dc=dev"
## OpenID Connect (pre-configured for local tests):
PHOTOPRISM_OIDC_ISSUER: "https://keycloak.localssl.dev/auth/realms/master"
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
PHOTOPRISM_OIDC_INSECURE: "true"
## Site Information
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"

View File

@@ -41,6 +41,7 @@ func TestMain(m *testing.M) {
// Init test config.
c := config.TestConfig()
get.SetConfig(c)
defer c.CloseDb()
// Increase login rate limit for testing.
limiter.Login = limiter.NewLimit(1, 10000)
@@ -48,9 +49,6 @@ func TestMain(m *testing.M) {
// Run unit tests.
code := m.Run()
// Close database connection.
_ = c.CloseDb()
os.Exit(code)
}

View File

@@ -0,0 +1,46 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n"
)
// OAuthAuthorize is a starting point for browser-based OpenID Connect flows.
//
// GET /api/v1/oauth/authorize
func OAuthAuthorize(router *gin.RouterGroup) {
router.GET("/oauth/authorize", func(c *gin.Context) {
// Prevent CDNs from caching this endpoint.
if header.IsCdn(c.Request) {
AbortNotFound(c)
return
}
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
// Get client IP address for logs and rate limiting checks.
clientIp := ClientIP(c)
actor := "unknown client"
action := "authorize"
// Abort if running in public mode.
if get.Config().Public() {
event.AuditErr([]string{clientIp, "oauth2", actor, action, authn.ErrDisabledInPublicMode.Error()})
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return
}
// TODO
// Send response.
c.JSON(http.StatusOK, gin.H{"status": StatusSuccess})
})
}

View File

@@ -0,0 +1,46 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n"
)
// OAuthLogout implements a single logout for OIDC-authenticated clients.
//
// GET /api/v1/oauth/logout
func OAuthLogout(router *gin.RouterGroup) {
router.GET("/oauth/logout", func(c *gin.Context) {
// Prevent CDNs from caching this endpoint.
if header.IsCdn(c.Request) {
AbortNotFound(c)
return
}
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
// Get client IP address for logs and rate limiting checks.
clientIp := ClientIP(c)
actor := "unknown client"
action := "logout"
// Abort if running in public mode.
if get.Config().Public() {
event.AuditErr([]string{clientIp, "oauth2", actor, action, authn.ErrDisabledInPublicMode.Error()})
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return
}
// TODO
// Send response.
c.JSON(http.StatusOK, gin.H{"status": StatusSuccess})
})
}

View File

@@ -0,0 +1,46 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n"
)
// OAuthRedirect creates a new access token for authenticated clients and then redirects back to the application.
//
// GET /api/v1/oauth/redirect
func OAuthRedirect(router *gin.RouterGroup) {
router.GET("/oauth/redirect", func(c *gin.Context) {
// Prevent CDNs from caching this endpoint.
if header.IsCdn(c.Request) {
AbortNotFound(c)
return
}
// Disable caching of responses.
c.Header(header.CacheControl, header.CacheControlNoStore)
// Get client IP address for logs and rate limiting checks.
clientIp := ClientIP(c)
actor := "unknown client"
action := "redirect"
// Abort if running in public mode.
if get.Config().Public() {
event.AuditErr([]string{clientIp, "oauth2", actor, action, authn.ErrDisabledInPublicMode.Error()})
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
return
}
// TODO
// Send response.
c.JSON(http.StatusOK, gin.H{"status": StatusSuccess})
})
}

View File

@@ -18,10 +18,10 @@ import (
"github.com/photoprism/photoprism/pkg/rnd"
)
// RevokeOAuthToken takes an access token and deletes it. A client may only delete its own tokens.
// OAuthRevoke takes an access token and deletes it. A client may only delete its own tokens.
//
// POST /api/v1/oauth/revoke
func RevokeOAuthToken(router *gin.RouterGroup) {
func OAuthRevoke(router *gin.RouterGroup) {
router.POST("/oauth/revoke", func(c *gin.Context) {
// Prevent CDNs from caching this endpoint.
if header.IsCdn(c.Request) {

View File

@@ -7,8 +7,6 @@ import (
"strings"
"testing"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
@@ -16,9 +14,10 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/rnd"
)
func TestRevokeOAuthToken(t *testing.T) {
func TestOAuthRevoke(t *testing.T) {
const tokenPath = "/api/v1/oauth/token"
const revokePath = "/api/v1/oauth/revoke"
@@ -27,8 +26,8 @@ func TestRevokeOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
RevokeOAuthToken(router)
OAuthToken(router)
OAuthRevoke(router)
data := url.Values{
"grant_type": {"client_credentials"},
@@ -63,8 +62,8 @@ func TestRevokeOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
RevokeOAuthToken(router)
OAuthToken(router)
OAuthRevoke(router)
createData := url.Values{
"grant_type": {"client_credentials"},
@@ -104,8 +103,8 @@ func TestRevokeOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
RevokeOAuthToken(router)
OAuthToken(router)
OAuthRevoke(router)
createData := url.Values{
"grant_type": {"client_credentials"},
@@ -145,8 +144,8 @@ func TestRevokeOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
RevokeOAuthToken(router)
OAuthToken(router)
OAuthRevoke(router)
createData := url.Values{
"grant_type": {"client_credentials"},
@@ -186,8 +185,8 @@ func TestRevokeOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
RevokeOAuthToken(router)
OAuthToken(router)
OAuthRevoke(router)
createData := url.Values{
"grant_type": {"client_credentials"},
@@ -225,7 +224,7 @@ func TestRevokeOAuthToken(t *testing.T) {
t.Run("PublicMode", func(t *testing.T) {
app, router, _ := NewApiTest()
RevokeOAuthToken(router)
OAuthRevoke(router)
sess := entity.SessionFixtures.Get("alice_token")
@@ -246,8 +245,8 @@ func TestRevokeOAuthToken(t *testing.T) {
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
CreateOAuthToken(router)
RevokeOAuthToken(router)
OAuthToken(router)
OAuthRevoke(router)
data := url.Values{
"grant_type": {"password"},

View File

@@ -18,11 +18,10 @@ import (
"github.com/photoprism/photoprism/pkg/header"
)
// CreateOAuthToken creates a new access token for clients that
// authenticate with valid OAuth2 client credentials.
// OAuthToken creates a new access token for clients that authenticate with valid OAuth2 client credentials.
//
// POST /api/v1/oauth/token
func CreateOAuthToken(router *gin.RouterGroup) {
func OAuthToken(router *gin.RouterGroup) {
router.POST("/oauth/token", func(c *gin.Context) {
// Prevent CDNs from caching this endpoint.
if header.IsCdn(c.Request) {

View File

@@ -7,19 +7,20 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/stretchr/testify/assert"
)
func TestCreateOAuthToken(t *testing.T) {
func TestOAuthToken(t *testing.T) {
t.Run("ClientSuccess", func(t *testing.T) {
app, router, conf := NewApiTest()
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -44,7 +45,7 @@ func TestCreateOAuthToken(t *testing.T) {
t.Run("PublicMode", func(t *testing.T) {
app, router, _ := NewApiTest()
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -71,7 +72,7 @@ func TestCreateOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -98,7 +99,7 @@ func TestCreateOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -124,7 +125,7 @@ func TestCreateOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -151,7 +152,7 @@ func TestCreateOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -178,7 +179,7 @@ func TestCreateOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -204,7 +205,7 @@ func TestCreateOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -234,7 +235,7 @@ func TestCreateOAuthToken(t *testing.T) {
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -263,7 +264,7 @@ func TestCreateOAuthToken(t *testing.T) {
conf.SetAuthMode(config.AuthModePasswd)
defer conf.SetAuthMode(config.AuthModePublic)
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -294,7 +295,7 @@ func TestCreateOAuthToken(t *testing.T) {
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"
@@ -325,7 +326,7 @@ func TestCreateOAuthToken(t *testing.T) {
sessId := AuthenticateUser(app, router, "deleted", "Deleted123!")
CreateOAuthToken(router)
OAuthToken(router)
var method = "POST"
var path = "/api/v1/oauth/token"

View File

@@ -16,8 +16,12 @@ func TestMain(m *testing.M) {
c := config.TestConfig()
defer c.CloseDb()
// Run unit tests.
code := m.Run()
// Close database connection.
_ = c.CloseDb()
os.Exit(code)
}

View File

@@ -20,11 +20,13 @@ func TestMain(m *testing.M) {
c := config.NewTestConfig("commands")
get.SetConfig(c)
defer c.CloseDb()
InitConfig = func(ctx *cli.Context) (*config.Config, error) {
return c, c.Init()
}
// Run unit tests.
code := m.Run()
os.Exit(code)

View File

@@ -25,7 +25,7 @@ var ConfigReports = []Report{
{Title: "Global Config Options", NoWrap: true, Report: func(conf *config.Config) ([][]string, []string) {
return conf.Report()
}},
{Title: "OpenID Connect", NoWrap: true, Report: func(conf *config.Config) ([][]string, []string) {
{Title: "OpenID Connect (OIDC)", NoWrap: true, Report: func(conf *config.Config) ([][]string, []string) {
return conf.OIDCReport()
}},
}

View File

@@ -3,24 +3,21 @@ package commands
import (
"testing"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/capture"
)
func TestShowConfigOptionsCommand(t *testing.T) {
var err error
ctx := config.CliTestContext()
ctx := NewTestContext(nil)
output := capture.Output(func() {
err = ShowConfigOptionsCommand.Run(ctx)
})
if err != nil {
t.Fatal(err)
}
assert.NoError(t, err)
assert.Contains(t, output, "PHOTOPRISM_IMPORT_PATH")
assert.Contains(t, output, "--sidecar-path")
assert.Contains(t, output, "sidecar `PATH`*optional*")

View File

@@ -11,26 +11,26 @@ const OIDCDefaultScopes = "openid email profile"
// OIDCEnabled checks if login via OpenID Connect (OIDC) is enabled.
func (c *Config) OIDCEnabled() bool {
return c.options.OIDCIssuer != "" && c.options.OIDCClient != "" && c.options.OIDCSecret != ""
return c.options.OIDCUri != "" && c.options.OIDCClient != "" && c.options.OIDCSecret != ""
}
// OIDCIssuer returns the OpenID Connect Issuer URL as string for single sign-on via OIDC.
func (c *Config) OIDCIssuer() string {
return c.options.OIDCIssuer
}
// OIDCIssuerURL returns the OpenID Connect Issuer URL as *url.URL for single sign-on via OIDC.
func (c *Config) OIDCIssuerURL() *url.URL {
if oidcIssuer := c.OIDCIssuer(); oidcIssuer == "" {
// OIDCUri returns the OpenID Connect issuer URI as *url.URL for single sign-on via OIDC.
func (c *Config) OIDCUri() *url.URL {
if uri := c.options.OIDCUri; uri == "" {
return &url.URL{}
} else if result, err := url.Parse(oidcIssuer); err != nil {
log.Errorf("oidc: failed to parse issuer URL (%s)", err)
} else if result, err := url.Parse(uri); err != nil {
log.Errorf("oidc: failed to parse issuer URI (%s)", err)
return &url.URL{}
} else {
return result
}
}
// OIDCInsecure checks if OIDC issuer SSL/TLS certificate verification should be skipped.
func (c *Config) OIDCInsecure() bool {
return c.options.OIDCInsecure
}
// OIDCClient returns the Client ID for single sign-on via OIDC.
func (c *Config) OIDCClient() string {
return c.options.OIDCClient
@@ -50,9 +50,14 @@ func (c *Config) OIDCScopes() string {
return c.options.OIDCScopes
}
// OIDCInsecure checks if OIDC issuer SSL/TLS certificate verification should be skipped.
func (c *Config) OIDCInsecure() bool {
return c.options.OIDCInsecure
// OIDCIcon returns the custom issuer icon URI for single sign-on via OIDC, if any.
func (c *Config) OIDCIcon() string {
return c.options.OIDCIcon
}
// OIDCButton returns the custom button text for single sign-on via OIDC, if any.
func (c *Config) OIDCButton() string {
return c.options.OIDCButton
}
// OIDCRegister checks if new accounts may be created via OIDC.
@@ -60,17 +65,25 @@ func (c *Config) OIDCRegister() bool {
return c.options.OIDCRegister
}
// OIDCRedirect checks if unauthenticated users should automatically be redirected to the OIDC login page.
func (c *Config) OIDCRedirect() bool {
return c.options.OIDCRedirect
}
// OIDCReport returns the OpenID Connect config values as a table for reporting.
func (c *Config) OIDCReport() (rows [][]string, cols []string) {
cols = []string{"Name", "Value"}
rows = [][]string{
{"oidc-issuer", c.OIDCIssuer()},
{"oidc-uri", c.OIDCUri().String()},
{"oidc-insecure", fmt.Sprintf("%t", c.OIDCInsecure())},
{"oidc-client", c.OIDCClient()},
{"oidc-secret", strings.Repeat("*", utf8.RuneCountInString(c.OIDCSecret()))},
{"oidc-scopes", c.OIDCScopes()},
{"oidc-insecure", fmt.Sprintf("%t", c.OIDCInsecure())},
{"oidc-icon", c.OIDCIcon()},
{"oidc-button", c.OIDCButton()},
{"oidc-register", fmt.Sprintf("%t", c.OIDCRegister())},
{"oidc-redirect", fmt.Sprintf("%t", c.OIDCRedirect())},
}
return rows, cols

View File

@@ -13,20 +13,24 @@ func TestConfig_OIDCEnabled(t *testing.T) {
assert.False(t, c.OIDCEnabled())
}
func TestConfig_OIDCIssuer(t *testing.T) {
func TestConfig_OIDCUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.OIDCIssuer())
assert.IsType(t, &url.URL{}, c.OIDCUri())
assert.Equal(t, "", c.OIDCUri().Path)
c.options.OIDCUri = "test"
assert.Equal(t, "test", c.OIDCUri().Path)
c.options.OIDCUri = ""
assert.IsType(t, &url.URL{}, c.OIDCUri())
assert.Equal(t, "", c.OIDCUri().String())
}
func TestConfig_OIDCIssuerURL(t *testing.T) {
func TestConfig_OIDCInsecure(t *testing.T) {
c := NewConfig(CliTestContext())
assert.IsType(t, &url.URL{}, c.OIDCIssuerURL())
assert.Equal(t, "", c.OIDCIssuerURL().Path)
c.options.OIDCIssuer = "test"
assert.Equal(t, "test", c.OIDCIssuerURL().Path)
assert.False(t, c.OIDCInsecure())
}
func TestConfig_OIDCClient(t *testing.T) {
@@ -51,16 +55,28 @@ func TestConfig_OIDCScopes(t *testing.T) {
assert.Equal(t, OIDCDefaultScopes, c.OIDCScopes())
}
func TestConfig_OIDCIcon(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.OIDCIcon())
}
func TestConfig_OIDCButton(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.OIDCButton())
}
func TestConfig_OIDCRegister(t *testing.T) {
c := NewConfig(CliTestContext())
assert.False(t, c.OIDCRegister())
}
func TestConfig_OIDCInsecure(t *testing.T) {
func TestConfig_OIDCRedirect(t *testing.T) {
c := NewConfig(CliTestContext())
assert.False(t, c.OIDCInsecure())
assert.False(t, c.OIDCRedirect())
}
func TestConfig_OIDCReport(t *testing.T) {

View File

@@ -20,11 +20,10 @@ func TestMain(m *testing.M) {
log.SetLevel(logrus.TraceLevel)
c := TestConfig()
defer c.CloseDb()
code := m.Run()
_ = c.CloseDb()
os.Exit(code)
}

View File

@@ -44,10 +44,15 @@ var Flags = CliFlags{
EnvVar: EnvVar("ADMIN_PASSWORD"),
}}, {
Flag: cli.StringFlag{
Name: "oidc-issuer",
Usage: "server `URI` for single sign-on via OpenID Connect (OIDC)",
Name: "oidc-uri",
Usage: "issuer `URI` for single sign-on via OpenID Connect (OIDC), e.g. https://accounts.google.com/o/oauth2/v2/auth",
Value: "",
EnvVar: EnvVar("OIDC_ISSUER"),
EnvVar: EnvVar("OIDC_URI"),
}}, {
Flag: cli.BoolFlag{
Name: "oidc-insecure",
Usage: "skip issuer SSL/TLS certificate verification",
EnvVar: EnvVar("OIDC_INSECURE"),
}}, {
Flag: cli.StringFlag{
Name: "oidc-client",
@@ -68,16 +73,28 @@ var Flags = CliFlags{
Value: OIDCDefaultScopes,
EnvVar: EnvVar("OIDC_SCOPES"),
}}, {
Flag: cli.BoolFlag{
Name: "oidc-insecure",
Usage: "skip issuer SSL/TLS certificate verification",
EnvVar: EnvVar("OIDC_INSECURE"),
Flag: cli.StringFlag{
Name: "oidc-icon",
Usage: "custom issuer icon `URI` for single sign-on via OIDC",
Value: "",
EnvVar: EnvVar("OIDC_ICON"),
}}, {
Flag: cli.StringFlag{
Name: "oidc-button",
Usage: "custom login button `TEXT` for single sign-on via OIDC",
Value: "",
EnvVar: EnvVar("OIDC_BUTTON"),
}}, {
Flag: cli.BoolFlag{
Name: "oidc-register",
Usage: "allow user registration via OIDC",
EnvVar: EnvVar("OIDC_REGISTER"),
}}, {
Flag: cli.BoolFlag{
Name: "oidc-redirect",
Usage: "automatically redirect unauthenticated users to the OIDC login page",
EnvVar: EnvVar("OIDC_REDIRECT"),
}}, {
Flag: cli.Int64Flag{
Name: "session-maxage",
Value: DefaultSessionMaxAge,

View File

@@ -31,12 +31,15 @@ type Options struct {
PasswordResetUri string `yaml:"PasswordResetUri" json:"-" flag:"password-reset-uri"`
RegisterUri string `yaml:"RegisterUri" json:"-" flag:"register-uri"`
LoginUri string `yaml:"LoginUri" json:"-" flag:"login-uri"`
OIDCIssuer string `yaml:"OIDCIssuer" json:"OIDCIssuer" flag:"oidc-issuer"`
OIDCUri string `yaml:"OIDCUri" json:"OIDCUri" flag:"oidc-uri"`
OIDCInsecure bool `yaml:"OIDCInsecure" json:"-" flag:"oidc-insecure"`
OIDCClient string `yaml:"OIDCClient" json:"-" flag:"oidc-client"`
OIDCSecret string `yaml:"OIDCSecret" json:"-" flag:"oidc-secret"`
OIDCScopes string `yaml:"OIDCScopes" json:"-" flag:"oidc-scopes"`
OIDCInsecure bool `yaml:"OIDCInsecure" json:"-" flag:"oidc-insecure"`
OIDCIcon string `yaml:"OIDCIcon" json:"-" flag:"oidc-icon"`
OIDCButton string `yaml:"OIDCButton" json:"-" flag:"oidc-button"`
OIDCRegister bool `yaml:"OIDCRegister" json:"-" flag:"oidc-register"`
OIDCRedirect bool `yaml:"OIDCRedirect" json:"-" flag:"oidc-redirect"`
SessionMaxAge int64 `yaml:"SessionMaxAge" json:"-" flag:"session-maxage"`
SessionTimeout int64 `yaml:"SessionTimeout" json:"-" flag:"session-timeout"`
SessionCache int64 `yaml:"SessionCache" json:"-" flag:"session-cache"`

View File

@@ -229,7 +229,7 @@ func CliTestContext() *cli.Context {
globalSet := flag.NewFlagSet("test", 0)
globalSet.String("config-path", config.ConfigPath, "doc")
globalSet.String("admin-password", config.DarktableBin, "doc")
globalSet.String("oidc-issuer", config.OIDCIssuer, "doc")
globalSet.String("oidc-uri", config.OIDCUri, "doc")
globalSet.String("oidc-client", config.OIDCClient, "doc")
globalSet.String("oidc-secret", config.OIDCSecret, "doc")
globalSet.String("oidc-scopes", config.OIDCScopes, "doc")
@@ -263,7 +263,7 @@ func CliTestContext() *cli.Context {
LogErr(c.Set("config-path", config.ConfigPath))
LogErr(c.Set("admin-password", config.AdminPassword))
LogErr(c.Set("oidc-issuer", config.OIDCIssuer))
LogErr(c.Set("oidc-uri", config.OIDCUri))
LogErr(c.Set("oidc-client", config.OIDCClient))
LogErr(c.Set("oidc-secret", config.OIDCSecret))
LogErr(c.Set("oidc-scopes", OIDCDefaultScopes))

View File

@@ -10,7 +10,7 @@ var onceOidc sync.Once
func initOidc() {
services.OIDC, _ = oidc.NewClient(
Config().OIDCIssuerURL(),
Config().OIDCUri(),
Config().OIDCClient(),
Config().OIDCSecret(),
Config().OIDCScopes(),
@@ -20,7 +20,7 @@ func initOidc() {
}
func OIDC() *oidc.Client {
oncePhotos.Do(initOidc)
onceOidc.Do(initOidc)
return services.OIDC
}

View File

@@ -14,10 +14,9 @@ func TestMain(m *testing.M) {
c := config.NewTestConfig("photoprism")
SetConfig(c)
defer c.CloseDb()
code := m.Run()
_ = c.CloseDb()
os.Exit(code)
}

View File

@@ -37,8 +37,11 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.CreateSession(APIv1)
api.GetSession(APIv1)
api.DeleteSession(APIv1)
api.CreateOAuthToken(APIv1)
api.RevokeOAuthToken(APIv1)
api.OAuthAuthorize(APIv1)
api.OAuthRedirect(APIv1)
api.OAuthToken(APIv1)
api.OAuthRevoke(APIv1)
api.OAuthLogout(APIv1)
// Server Config.
api.GetConfigOptions(APIv1)

View File

@@ -21,6 +21,7 @@ func TestMain(m *testing.M) {
// Init test config.
c := config.TestConfig()
get.SetConfig(c)
defer c.CloseDb()
// Increase login rate limit for testing.
limiter.Login = limiter.NewLimit(1, 10000)
@@ -28,8 +29,5 @@ func TestMain(m *testing.M) {
// Run unit tests.
code := m.Run()
// Close database connection.
_ = c.CloseDb()
os.Exit(code)
}

View File

@@ -15,6 +15,8 @@ func TestMain(m *testing.M) {
log.SetLevel(logrus.TraceLevel)
event.AuditLog = log
defer Shutdown()
code := m.Run()
// Remove generated test files and folders.
@@ -22,8 +24,6 @@ func TestMain(m *testing.M) {
_ = os.RemoveAll("testdata/cache")
_ = os.RemoveAll("testdata/vips")
Shutdown()
os.Exit(code)
}