2023-12-12 18:42:50 +01:00
package api
import (
"net/http"
2024-01-08 16:57:07 +01:00
"github.com/dustin/go-humanize/english"
2023-12-12 18:42:50 +01:00
"github.com/gin-gonic/gin"
"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"
2024-01-08 16:57:07 +01:00
"github.com/photoprism/photoprism/pkg/authn"
2024-01-08 14:53:39 +01:00
"github.com/photoprism/photoprism/pkg/clean"
2024-01-09 10:58:47 +01:00
"github.com/photoprism/photoprism/pkg/header"
2024-01-08 14:53:39 +01:00
"github.com/photoprism/photoprism/pkg/rnd"
2023-12-12 18:42:50 +01:00
)
2024-01-08 14:53:39 +01:00
// CreateOAuthToken creates a new access token for clients that
2024-01-06 17:35:19 +01:00
// authenticate with valid OAuth2 client credentials.
2023-12-12 18:42:50 +01:00
//
// POST /api/v1/oauth/token
2024-01-08 14:53:39 +01:00
func CreateOAuthToken ( router * gin . RouterGroup ) {
2023-12-12 18:42:50 +01:00
router . POST ( "/oauth/token" , func ( c * gin . Context ) {
2024-01-16 16:17:16 +01:00
// Prevent CDNs from caching this endpoint.
if header . IsCdn ( c . Request ) {
2024-01-16 20:56:43 +01:00
AbortNotFound ( c )
2024-01-16 16:17:16 +01:00
return
}
2024-01-08 14:53:39 +01:00
// Get client IP address for logs and rate limiting checks.
2024-01-14 18:28:17 +01:00
clientIp := ClientIP ( c )
2024-01-08 14:53:39 +01:00
if get . Config ( ) . Public ( ) {
2024-01-16 16:17:16 +01:00
// Abort if running in public mode.
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client" , "create session" , "oauth2" , "disabled in public mode" } )
2024-01-16 16:17:16 +01:00
AbortForbidden ( c )
2024-01-08 14:53:39 +01:00
return
}
2023-12-12 18:42:50 +01:00
var err error
2024-01-10 12:21:43 +01:00
// Client authentication request credentials.
2023-12-12 18:42:50 +01:00
var f form . ClientCredentials
// Allow authentication with basic auth and form values.
2024-01-09 10:58:47 +01:00
if clientId , clientSecret , _ := header . BasicAuth ( c ) ; clientId != "" && clientSecret != "" {
2023-12-12 18:42:50 +01:00
f . ClientID = clientId
f . ClientSecret = clientSecret
2024-01-10 12:21:43 +01:00
} else if err = c . ShouldBind ( & f ) ; err != nil {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client" , "create session" , "oauth2" , "%s" } , err )
2023-12-12 18:42:50 +01:00
AbortBadRequest ( c )
return
}
// Check the credentials for completeness and the correct format.
if err = f . Validate ( ) ; err != nil {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client" , "create session" , "oauth2" , "%s" } , err )
2023-12-12 18:42:50 +01:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "error" : i18n . Msg ( i18n . ErrInvalidCredentials ) } )
return
}
2024-01-17 14:16:02 +01:00
// Disable caching of responses.
c . Header ( header . CacheControl , header . CacheControlNoStore )
2024-01-14 18:28:17 +01:00
// Fail if authentication error rate limit is exceeded.
if clientIp != "" && ( limiter . Login . Reject ( clientIp ) || limiter . Auth . Reject ( clientIp ) ) {
2023-12-12 18:42:50 +01:00
limiter . AbortJSON ( c )
return
}
// Find the client that has the ID specified in the authentication request.
2024-01-18 16:53:05 +01:00
client := entity . FindClientByUID ( f . ClientID )
2023-12-12 18:42:50 +01:00
// Abort if the client ID or secret are invalid.
if client == nil {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client %s" , "create session" , "oauth2" , "invalid client id" } , f . ClientID )
2023-12-12 18:42:50 +01:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "error" : i18n . Msg ( i18n . ErrInvalidCredentials ) } )
2024-01-14 18:28:17 +01:00
limiter . Login . Reserve ( clientIp )
2023-12-12 18:42:50 +01:00
return
} else if ! client . AuthEnabled {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client %s" , "create session" , "oauth2" , "authentication disabled" } , f . ClientID )
2023-12-12 18:42:50 +01:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "error" : i18n . Msg ( i18n . ErrInvalidCredentials ) } )
return
2024-01-08 14:53:39 +01:00
} else if method := client . Method ( ) ; ! method . IsDefault ( ) && method != authn . MethodOAuth2 {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client %s" , "create session" , "oauth2" , "method %s not supported" } , f . ClientID , clean . LogQuote ( method . String ( ) ) )
2023-12-12 18:42:50 +01:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "error" : i18n . Msg ( i18n . ErrInvalidCredentials ) } )
return
} else if client . WrongSecret ( f . ClientSecret ) {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client %s" , "create session" , "oauth2" , "invalid client secret" } , f . ClientID )
2023-12-12 18:42:50 +01:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H { "error" : i18n . Msg ( i18n . ErrInvalidCredentials ) } )
2024-01-14 18:28:17 +01:00
limiter . Login . Reserve ( clientIp )
2023-12-12 18:42:50 +01:00
return
}
// Create new client session.
sess := client . NewSession ( c )
// Try to log in and save session if successful.
if sess , err = get . Session ( ) . Save ( sess ) ; err != nil {
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client %s" , "create session" , "oauth2" , "%s" } , f . ClientID , err )
2023-12-12 18:42:50 +01:00
c . AbortWithStatusJSON ( sess . HttpStatus ( ) , gin . H { "error" : i18n . Msg ( i18n . ErrInvalidCredentials ) } )
return
} else if sess == nil {
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client %s" , "create session" , "oauth2" , StatusFailed . String ( ) } , f . ClientID )
2023-12-12 18:42:50 +01:00
c . AbortWithStatusJSON ( sess . HttpStatus ( ) , gin . H { "error" : i18n . Msg ( i18n . ErrUnexpected ) } )
return
} else {
2024-01-29 17:32:53 +01:00
event . AuditInfo ( [ ] string { clientIp , "client %s" , "session %s" , "oauth2" , "created" } , f . ClientID , sess . RefID )
2023-12-12 18:42:50 +01:00
}
2024-01-08 16:57:07 +01:00
// Deletes old client sessions above the configured limit.
if deleted := client . EnforceAuthTokenLimit ( ) ; deleted > 0 {
2024-01-29 21:08:01 +01:00
event . AuditInfo ( [ ] string { clientIp , "client %s" , "session %s" , "oauth2" , "deleted %s" } , f . ClientID , sess . RefID , english . Plural ( deleted , "previously created client session" , "previously created client sessions" ) )
2024-01-08 16:57:07 +01:00
}
2024-01-06 17:35:19 +01:00
// Response includes access token, token type, and token lifetime.
2023-12-12 18:42:50 +01:00
data := gin . H {
2024-01-06 17:35:19 +01:00
"access_token" : sess . AuthToken ( ) ,
"token_type" : sess . AuthTokenType ( ) ,
2023-12-12 18:42:50 +01:00
"expires_in" : sess . ExpiresIn ( ) ,
}
2024-01-06 17:35:19 +01:00
// Return JSON response.
2023-12-12 18:42:50 +01:00
c . JSON ( http . StatusOK , data )
} )
}
2024-01-08 14:53:39 +01:00
2024-01-10 12:21:43 +01:00
// RevokeOAuthToken takes an access token and deletes it. A client may only delete its own tokens.
2024-01-08 14:53:39 +01:00
//
2024-01-10 12:21:43 +01:00
// POST /api/v1/oauth/revoke
func RevokeOAuthToken ( router * gin . RouterGroup ) {
router . POST ( "/oauth/revoke" , func ( c * gin . Context ) {
2024-01-16 16:17:16 +01:00
// Prevent CDNs from caching this endpoint.
if header . IsCdn ( c . Request ) {
2024-01-16 20:56:43 +01:00
AbortNotFound ( c )
2024-01-16 16:17:16 +01:00
return
}
2024-01-08 14:53:39 +01:00
// Get client IP address for logs and rate limiting checks.
2024-01-14 18:28:17 +01:00
clientIp := ClientIP ( c )
2024-01-08 14:53:39 +01:00
// Abort if running in public mode.
if get . Config ( ) . Public ( ) {
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client" , "delete session" , "oauth2" , "disabled in public mode" } )
2024-01-08 14:53:39 +01:00
Abort ( c , http . StatusForbidden , i18n . ErrForbidden )
return
}
2024-01-10 12:21:43 +01:00
var err error
// Token revocation request data.
var f form . ClientToken
authToken := AuthToken ( c )
// Get the auth token to be revoked from the submitted form values or the request header.
if err = c . ShouldBind ( & f ) ; err != nil && authToken == "" {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client" , "delete session" , "oauth2" , "%s" } , err )
2024-01-10 12:21:43 +01:00
AbortBadRequest ( c )
return
} else if f . Empty ( ) {
f . AuthToken = authToken
f . TypeHint = form . ClientAccessToken
}
// Check the token form values.
if err = f . Validate ( ) ; err != nil {
2024-01-29 17:32:53 +01:00
event . AuditWarn ( [ ] string { clientIp , "client" , "delete session" , "oauth2" , "%s" } , err )
2024-01-10 12:21:43 +01:00
AbortBadRequest ( c )
return
}
2024-01-17 14:16:02 +01:00
// Disable caching of responses.
c . Header ( header . CacheControl , header . CacheControlNoStore )
2024-01-08 14:53:39 +01:00
// Find session based on auth token.
2024-01-10 12:21:43 +01:00
sess , err := entity . FindSession ( rnd . SessionID ( f . AuthToken ) )
2024-01-08 14:53:39 +01:00
if err != nil {
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client %s" , "session %s" , "delete session as %s" , "oauth2" , "%s" } , clean . Log ( sess . ClientInfo ( ) ) , clean . Log ( sess . RefID ) , sess . ClientRole ( ) . String ( ) , err . Error ( ) )
2024-01-08 14:53:39 +01:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , i18n . NewResponse ( http . StatusUnauthorized , i18n . ErrUnauthorized ) )
return
} else if sess == nil {
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client %s" , "session %s" , "delete session as %s" , "oauth2" , "denied" } , clean . Log ( sess . ClientInfo ( ) ) , clean . Log ( sess . RefID ) , sess . ClientRole ( ) . String ( ) )
2024-01-08 14:53:39 +01:00
c . AbortWithStatusJSON ( http . StatusUnauthorized , i18n . NewResponse ( http . StatusUnauthorized , i18n . ErrUnauthorized ) )
return
} else if sess . Abort ( c ) {
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client %s" , "session %s" , "delete session as %s" , "oauth2" , "denied" } , clean . Log ( sess . ClientInfo ( ) ) , clean . Log ( sess . RefID ) , sess . ClientRole ( ) . String ( ) )
2024-01-08 14:53:39 +01:00
return
} else if ! sess . IsClient ( ) {
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client %s" , "session %s" , "delete session as %s" , "oauth2" , "denied" } , clean . Log ( sess . ClientInfo ( ) ) , clean . Log ( sess . RefID ) , sess . ClientRole ( ) . String ( ) )
2024-01-08 14:53:39 +01:00
c . AbortWithStatusJSON ( http . StatusForbidden , i18n . NewResponse ( http . StatusForbidden , i18n . ErrForbidden ) )
return
} else {
2024-01-29 17:32:53 +01:00
event . AuditInfo ( [ ] string { clientIp , "client %s" , "session %s" , "delete session as %s" , "oauth2" , "granted" } , clean . Log ( sess . ClientInfo ( ) ) , clean . Log ( sess . RefID ) , sess . ClientRole ( ) . String ( ) )
2024-01-08 14:53:39 +01:00
}
// Delete session cache and database record.
if err = sess . Delete ( ) ; err != nil {
// Log error.
2024-01-29 17:32:53 +01:00
event . AuditErr ( [ ] string { clientIp , "client %s" , "session %s" , "delete session as %s" , "oauth2" , "%s" } , clean . Log ( sess . ClientInfo ( ) ) , clean . Log ( sess . RefID ) , sess . ClientRole ( ) . String ( ) , err )
2024-01-08 14:53:39 +01:00
// Return JSON error.
c . AbortWithStatusJSON ( http . StatusNotFound , i18n . NewResponse ( http . StatusNotFound , i18n . ErrNotFound ) )
return
}
// Log event.
2024-01-29 17:32:53 +01:00
event . AuditInfo ( [ ] string { clientIp , "client %s" , "session %s" , "oauth2" , "deleted" } , clean . Log ( sess . ClientInfo ( ) ) , clean . Log ( sess . RefID ) )
2024-01-08 14:53:39 +01:00
// Return JSON response for confirmation.
c . JSON ( http . StatusOK , DeleteSessionResponse ( sess . ID ) )
} )
}