From 7dff5511bc5afc4413e8d8fffe60d676b09ac3d5 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Thu, 27 Jun 2024 10:16:12 +0200 Subject: [PATCH] API: Refactor OAuth2 and OIDC endpoints #782 Signed-off-by: Michael Mayer --- internal/api/oauth_authorize.go | 2 +- .../{oauth_redirect.go => oauth_userinfo.go} | 12 ++--- internal/api/oidc_login.go | 4 +- internal/api/oidc_redirect.go | 46 +++++++++++++++++++ internal/server/routes.go | 17 ++++--- internal/server/wellknown/oauth.go | 2 +- internal/server/wellknown/openid.go | 4 +- 7 files changed, 69 insertions(+), 18 deletions(-) rename internal/api/{oauth_redirect.go => oauth_userinfo.go} (73%) create mode 100644 internal/api/oidc_redirect.go diff --git a/internal/api/oauth_authorize.go b/internal/api/oauth_authorize.go index e13f42319..153cd60d9 100644 --- a/internal/api/oauth_authorize.go +++ b/internal/api/oauth_authorize.go @@ -41,6 +41,6 @@ func OAuthAuthorize(router *gin.RouterGroup) { // TODO // Send response. - c.JSON(http.StatusOK, gin.H{"status": StatusSuccess}) + c.JSON(http.StatusMethodNotAllowed, gin.H{"status": StatusFailed}) }) } diff --git a/internal/api/oauth_redirect.go b/internal/api/oauth_userinfo.go similarity index 73% rename from internal/api/oauth_redirect.go rename to internal/api/oauth_userinfo.go index 0a4c29526..c868b7a19 100644 --- a/internal/api/oauth_redirect.go +++ b/internal/api/oauth_userinfo.go @@ -12,11 +12,11 @@ import ( "github.com/photoprism/photoprism/pkg/i18n" ) -// OAuthRedirect creates a new access token for authenticated clients and then redirects back to the application. +// OAuthUserinfo returns information about the authenticated user. // -// GET /api/v1/oauth/redirect -func OAuthRedirect(router *gin.RouterGroup) { - router.GET("/oauth/redirect", func(c *gin.Context) { +// GET /api/v1/oauth/userinfo +func OAuthUserinfo(router *gin.RouterGroup) { + router.GET("/oauth/userinfo", func(c *gin.Context) { // Prevent CDNs from caching this endpoint. if header.IsCdn(c.Request) { AbortNotFound(c) @@ -29,7 +29,7 @@ func OAuthRedirect(router *gin.RouterGroup) { // Get client IP address for logs and rate limiting checks. clientIp := ClientIP(c) actor := "unknown client" - action := "redirect" + action := "userinfo" // Abort if running in public mode. if get.Config().Public() { @@ -41,6 +41,6 @@ func OAuthRedirect(router *gin.RouterGroup) { // TODO // Send response. - c.JSON(http.StatusOK, gin.H{"status": StatusSuccess}) + c.JSON(http.StatusMethodNotAllowed, gin.H{"status": StatusFailed}) }) } diff --git a/internal/api/oidc_login.go b/internal/api/oidc_login.go index 4d582536c..4b8a45035 100644 --- a/internal/api/oidc_login.go +++ b/internal/api/oidc_login.go @@ -12,7 +12,7 @@ import ( "github.com/photoprism/photoprism/pkg/i18n" ) -// OIDCLogin redirects to the login page of the configured OpenID Connect provider, if any. +// OIDCLogin redirects a browser to the login page of the configured OpenID Connect provider, if any. // // GET /api/v1/oidc/login func OIDCLogin(router *gin.RouterGroup) { @@ -41,6 +41,6 @@ func OIDCLogin(router *gin.RouterGroup) { // TODO // Send response. - c.JSON(http.StatusOK, gin.H{"status": StatusSuccess}) + c.JSON(http.StatusMethodNotAllowed, gin.H{"status": StatusFailed}) }) } diff --git a/internal/api/oidc_redirect.go b/internal/api/oidc_redirect.go new file mode 100644 index 000000000..cb1b4b5d6 --- /dev/null +++ b/internal/api/oidc_redirect.go @@ -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" +) + +// OIDCRedirect creates a new access token for authenticated users and then redirects the browser back to the app. +// +// GET /api/v1/oidc/redirect +func OIDCRedirect(router *gin.RouterGroup) { + router.GET("/oidc/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, "oidc", actor, action, authn.ErrDisabledInPublicMode.Error()}) + Abort(c, http.StatusForbidden, i18n.ErrForbidden) + return + } + + // TODO + + // Send response. + c.JSON(http.StatusMethodNotAllowed, gin.H{"status": StatusFailed}) + }) +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 4c1d0d2c8..1d712804b 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -33,27 +33,32 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { // Register JSON REST-API version 1 (APIv1) routes, grouped by functionality. // Docs: https://pkg.go.dev/github.com/photoprism/photoprism/internal/api - // Authentication. + // User Sessions. api.CreateSession(APIv1) api.GetSession(APIv1) api.DeleteSession(APIv1) + + // OAuth2 Client Endpoints. api.OAuthAuthorize(APIv1) - api.OAuthRedirect(APIv1) + api.OAuthUserinfo(APIv1) api.OAuthToken(APIv1) api.OAuthRevoke(APIv1) - api.OIDCLogin(APIv1) - // Server Config. + // OIDC Client Endpoints. + api.OIDCLogin(APIv1) + api.OIDCRedirect(APIv1) + + // Global Configuration. api.GetConfigOptions(APIv1) api.SaveConfigOptions(APIv1) api.StopServer(APIv1) - // Custom Settings. + // User Settings. api.GetClientConfig(APIv1) api.GetSettings(APIv1) api.SaveSettings(APIv1) - // Profile and Uploads. + // User Profile and Uploads. api.UploadUserFiles(APIv1) api.ProcessUserUpload(APIv1) api.UploadUserAvatar(APIv1) diff --git a/internal/server/wellknown/oauth.go b/internal/server/wellknown/oauth.go index 0036c747e..58a7f57b2 100644 --- a/internal/server/wellknown/oauth.go +++ b/internal/server/wellknown/oauth.go @@ -43,7 +43,7 @@ type OAuthAuthorizationServer struct { func NewOAuthAuthorizationServer(conf *config.Config) *OAuthAuthorizationServer { return &OAuthAuthorizationServer{ Issuer: conf.SiteUrl(), - AuthorizationEndpoint: "", + AuthorizationEndpoint: fmt.Sprintf("%sapi/v1/oauth/authorize", conf.SiteUrl()), TokenEndpoint: fmt.Sprintf("%sapi/v1/oauth/token", conf.SiteUrl()), ScopesSupported: acl.Rules.Resources(), ResponseTypesSupported: OAuthResponseTypes, diff --git a/internal/server/wellknown/openid.go b/internal/server/wellknown/openid.go index 193822e84..6c0ec426a 100644 --- a/internal/server/wellknown/openid.go +++ b/internal/server/wellknown/openid.go @@ -39,9 +39,9 @@ type OpenIDConfiguration struct { func NewOpenIDConfiguration(conf *config.Config) *OpenIDConfiguration { return &OpenIDConfiguration{ Issuer: conf.SiteUrl(), - AuthorizationEndpoint: "", + AuthorizationEndpoint: fmt.Sprintf("%sapi/v1/oauth/authorize", conf.SiteUrl()), TokenEndpoint: fmt.Sprintf("%sapi/v1/oauth/token", conf.SiteUrl()), - UserinfoEndpoint: "", + UserinfoEndpoint: fmt.Sprintf("%sapi/v1/oauth/userinfo", conf.SiteUrl()), RegistrationEndpoint: "", JwksUri: "", ResponseTypesSupported: OAuthResponseTypes,