mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
105
internal/api/session_oauth.go
Normal file
105
internal/api/session_oauth.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
)
|
||||
|
||||
// CreateOauthToken creates a new access token for clients that
|
||||
// authenticate with valid OAuth2 client credentials.
|
||||
//
|
||||
// POST /api/v1/oauth/token
|
||||
func CreateOauthToken(router *gin.RouterGroup) {
|
||||
router.POST("/oauth/token", func(c *gin.Context) {
|
||||
// client_id, client_secret
|
||||
var err error
|
||||
var f form.ClientCredentials
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIP := ClientIP(c)
|
||||
|
||||
// Allow authentication with basic auth and form values.
|
||||
if clientId, clientSecret, _ := BasicAuth(c); clientId != "" && clientSecret != "" {
|
||||
f.ClientID = clientId
|
||||
f.ClientSecret = clientSecret
|
||||
} else if err = c.Bind(&f); err != nil {
|
||||
event.AuditWarn([]string{clientIP, "oauth", "%s"}, err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Check the credentials for completeness and the correct format.
|
||||
if err = f.Validate(); err != nil {
|
||||
event.AuditWarn([]string{clientIP, "oauth", "%s"}, err)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
}
|
||||
|
||||
// Check limit for failed auth requests (max. 10 per minute).
|
||||
if limiter.Login.Reject(clientIP) {
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the client that has the ID specified in the authentication request.
|
||||
client := entity.FindClient(f.ClientID)
|
||||
|
||||
// Abort if the client ID or secret are invalid.
|
||||
if client == nil {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create access token", "invalid client id"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
limiter.Login.Reserve(clientIP)
|
||||
return
|
||||
} else if !client.AuthEnabled {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create access token", "running in public mode"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if client.AuthMethod != authn.MethodOAuth2.String() {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create access token", "%s authentication not supported"}, f.ClientID, client.AuthMethod)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if client.WrongSecret(f.ClientSecret) {
|
||||
event.AuditWarn([]string{clientIP, "client %s", "create access token", "invalid client secret"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
limiter.Login.Reserve(clientIP)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new client session.
|
||||
sess := client.NewSession(c)
|
||||
|
||||
// TODO: Enforce limit for maximum number of access tokens.
|
||||
|
||||
// Try to log in and save session if successful.
|
||||
if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{clientIP, "client %s", "create access token", "%s"}, f.ClientID, err)
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIP, "client %s", "create access token", "failed unexpectedly"}, f.ClientID)
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrUnexpected)})
|
||||
return
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIP, "client %s", "session %s", "access token created"}, f.ClientID, sess.RefID)
|
||||
}
|
||||
|
||||
// Response includes access token, token type, and token lifetime.
|
||||
data := gin.H{
|
||||
"access_token": sess.AuthToken(),
|
||||
"token_type": sess.AuthTokenType(),
|
||||
"expires_in": sess.ExpiresIn(),
|
||||
}
|
||||
|
||||
// Return JSON response.
|
||||
c.JSON(http.StatusOK, data)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user