2023-06-17 21:25:46 +08:00
package v1
2022-02-03 15:32:03 +08:00
import (
2022-02-04 16:51:48 +08:00
"encoding/json"
2022-02-03 15:32:03 +08:00
"fmt"
"net/http"
2023-02-19 09:50:30 +08:00
"regexp"
2022-02-03 15:32:03 +08:00
2023-06-17 21:25:46 +08:00
"github.com/labstack/echo/v4"
2023-01-01 23:55:02 +08:00
"github.com/pkg/errors"
2023-07-06 22:53:38 +08:00
"github.com/usememos/memos/common/util"
2023-02-19 09:50:30 +08:00
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/plugin/idp/oauth2"
"github.com/usememos/memos/store"
2022-02-06 16:19:20 +08:00
"golang.org/x/crypto/bcrypt"
2022-02-03 15:32:03 +08:00
)
2023-06-17 21:25:46 +08:00
type SignIn struct {
Username string ` json:"username" `
Password string ` json:"password" `
}
type SSOSignIn struct {
2023-08-04 21:55:07 +08:00
IdentityProviderID int32 ` json:"identityProviderId" `
2023-06-17 21:25:46 +08:00
Code string ` json:"code" `
RedirectURI string ` json:"redirectUri" `
}
type SignUp struct {
Username string ` json:"username" `
Password string ` json:"password" `
}
2023-07-01 00:03:28 +08:00
func ( s * APIV1Service ) registerAuthRoutes ( g * echo . Group ) {
2023-08-09 22:30:27 +08:00
g . POST ( "/auth/signin" , s . SignIn )
g . POST ( "/auth/signin/sso" , s . SignInSSO )
g . POST ( "/auth/signout" , s . SignOut )
g . POST ( "/auth/signup" , s . SignUp )
2023-08-09 21:53:06 +08:00
}
2023-07-30 21:22:02 +08:00
2023-08-09 22:30:27 +08:00
// SignIn godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Sign-in to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignIn true "Sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
// @Failure 403 {object} nil "User has been archived with username %s"
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signin [POST]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) SignIn ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
signin := & SignIn { }
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
disablePasswordLoginSystemSetting , err := s . Store . GetSystemSetting ( ctx , & store . FindSystemSetting {
Name : SystemSettingDisablePasswordLoginName . String ( ) ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find system setting" ) . SetInternal ( err )
}
if disablePasswordLoginSystemSetting != nil {
disablePasswordLogin := false
err = json . Unmarshal ( [ ] byte ( disablePasswordLoginSystemSetting . Value ) , & disablePasswordLogin )
2023-07-06 22:53:38 +08:00
if err != nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to unmarshal system setting" ) . SetInternal ( err )
2022-02-03 15:32:03 +08:00
}
2023-08-09 21:53:06 +08:00
if disablePasswordLogin {
return echo . NewHTTPError ( http . StatusUnauthorized , "Password login is deactivated" )
2022-02-03 15:32:03 +08:00
}
2023-08-09 21:53:06 +08:00
}
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( signin ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted signin request" ) . SetInternal ( err )
}
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
user , err := s . Store . GetUser ( ctx , & store . FindUser {
Username : & signin . Username ,
2022-02-03 15:32:03 +08:00
} )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Incorrect login credentials, please try again" )
}
if user == nil {
return echo . NewHTTPError ( http . StatusUnauthorized , "Incorrect login credentials, please try again" )
} else if user . RowStatus == store . Archived {
return echo . NewHTTPError ( http . StatusForbidden , fmt . Sprintf ( "User has been archived with username %s" , signin . Username ) )
}
2022-02-18 22:21:10 +08:00
2023-08-09 21:53:06 +08:00
// Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt . CompareHashAndPassword ( [ ] byte ( user . PasswordHash ) , [ ] byte ( signin . Password ) ) ; err != nil {
// If the two passwords don't match, return a 401 status.
return echo . NewHTTPError ( http . StatusUnauthorized , "Incorrect login credentials, please try again" )
}
2023-02-19 09:50:30 +08:00
2023-08-09 21:53:06 +08:00
if err := GenerateTokensAndSetCookies ( c , user , s . Secret ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate tokens" ) . SetInternal ( err )
}
if err := s . createAuthSignInActivity ( c , user ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create activity" ) . SetInternal ( err )
}
userMessage := convertUserFromStore ( user )
return c . JSON ( http . StatusOK , userMessage )
}
2023-02-19 09:50:30 +08:00
2023-08-09 22:30:27 +08:00
// SignInSSO godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Sign-in to memos using SSO.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SSOSignIn true "SSO sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} nil "Access denied, identifier does not match the filter."
// @Failure 403 {object} nil "User has been archived with username {username}"
// @Failure 404 {object} nil "Identity provider not found"
// @Failure 500 {object} nil "Failed to find identity provider | Failed to create identity provider instance | Failed to exchange token | Failed to get user info | Failed to compile identifier filter | Incorrect login credentials, please try again | Failed to generate random password | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signin/sso [POST]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) SignInSSO ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
signin := & SSOSignIn { }
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( signin ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted signin request" ) . SetInternal ( err )
}
2023-02-19 09:50:30 +08:00
2023-08-09 21:53:06 +08:00
identityProvider , err := s . Store . GetIdentityProvider ( ctx , & store . FindIdentityProvider {
ID : & signin . IdentityProviderID ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find identity provider" ) . SetInternal ( err )
}
if identityProvider == nil {
return echo . NewHTTPError ( http . StatusNotFound , "Identity provider not found" )
}
2023-02-19 09:50:30 +08:00
2023-08-09 21:53:06 +08:00
var userInfo * idp . IdentityProviderUserInfo
if identityProvider . Type == store . IdentityProviderOAuth2Type {
oauth2IdentityProvider , err := oauth2 . NewIdentityProvider ( identityProvider . Config . OAuth2Config )
2023-07-06 22:53:38 +08:00
if err != nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create identity provider instance" ) . SetInternal ( err )
2023-02-19 09:50:30 +08:00
}
2023-08-09 21:53:06 +08:00
token , err := oauth2IdentityProvider . ExchangeToken ( ctx , signin . RedirectURI , signin . Code )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to exchange token" ) . SetInternal ( err )
2023-02-19 09:50:30 +08:00
}
2023-08-09 21:53:06 +08:00
userInfo , err = oauth2IdentityProvider . UserInfo ( token )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to get user info" ) . SetInternal ( err )
2022-11-03 21:47:36 +08:00
}
2023-08-09 21:53:06 +08:00
}
2022-11-03 21:47:36 +08:00
2023-08-09 21:53:06 +08:00
identifierFilter := identityProvider . IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex , err := regexp . Compile ( identifierFilter )
2023-02-11 15:15:56 +08:00
if err != nil {
2023-08-09 21:53:06 +08:00
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to compile identifier filter" ) . SetInternal ( err )
2023-02-11 15:15:56 +08:00
}
2023-08-09 21:53:06 +08:00
if ! identifierFilterRegex . MatchString ( userInfo . Identifier ) {
return echo . NewHTTPError ( http . StatusUnauthorized , "Access denied, identifier does not match the filter." ) . SetInternal ( err )
}
}
2023-06-26 23:06:53 +08:00
2023-08-09 21:53:06 +08:00
user , err := s . Store . GetUser ( ctx , & store . FindUser {
Username : & userInfo . Identifier ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Incorrect login credentials, please try again" )
}
if user == nil {
2023-08-25 23:10:51 +08:00
allowSignUpSetting , err := s . Store . GetSystemSetting ( ctx , & store . FindSystemSetting {
Name : SystemSettingAllowSignUpName . String ( ) ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find system setting" ) . SetInternal ( err )
}
allowSignUpSettingValue := false
if allowSignUpSetting != nil {
err = json . Unmarshal ( [ ] byte ( allowSignUpSetting . Value ) , & allowSignUpSettingValue )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to unmarshal system setting allow signup" ) . SetInternal ( err )
}
}
if ! allowSignUpSettingValue {
return echo . NewHTTPError ( http . StatusUnauthorized , "signup is disabled" ) . SetInternal ( err )
}
2023-07-02 14:27:23 +08:00
userCreate := & store . User {
2023-08-09 21:53:06 +08:00
Username : userInfo . Identifier ,
2023-06-26 23:06:53 +08:00
// The new signup user should be normal user by default.
2023-07-02 18:56:25 +08:00
Role : store . RoleUser ,
2023-08-09 21:53:06 +08:00
Nickname : userInfo . DisplayName ,
Email : userInfo . Email ,
2023-07-06 22:53:38 +08:00
OpenID : util . GenUUID ( ) ,
2023-06-26 23:06:53 +08:00
}
2023-08-09 21:53:06 +08:00
password , err := util . RandomString ( 20 )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate random password" ) . SetInternal ( err )
2022-02-03 15:32:03 +08:00
}
2023-08-09 21:53:06 +08:00
passwordHash , err := bcrypt . GenerateFromPassword ( [ ] byte ( password ) , bcrypt . DefaultCost )
2022-02-06 16:19:20 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate password hash" ) . SetInternal ( err )
}
2022-08-20 21:03:15 +08:00
userCreate . PasswordHash = string ( passwordHash )
2023-08-09 21:53:06 +08:00
user , err = s . Store . CreateUser ( ctx , userCreate )
2022-02-03 15:32:03 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create user" ) . SetInternal ( err )
}
2023-08-09 21:53:06 +08:00
}
if user . RowStatus == store . Archived {
return echo . NewHTTPError ( http . StatusForbidden , fmt . Sprintf ( "User has been archived with username %s" , userInfo . Identifier ) )
}
if err := GenerateTokensAndSetCookies ( c , user , s . Secret ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate tokens" ) . SetInternal ( err )
}
if err := s . createAuthSignInActivity ( c , user ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create activity" ) . SetInternal ( err )
}
userMessage := convertUserFromStore ( user )
return c . JSON ( http . StatusOK , userMessage )
}
2023-08-09 22:30:27 +08:00
// SignOut godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Sign-out from memos.
// @Tags auth
// @Produce json
// @Success 200 {boolean} true "Sign-out success"
// @Router /api/v1/auth/signout [POST]
2023-08-09 22:30:27 +08:00
func ( * APIV1Service ) SignOut ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
RemoveTokensAndCookies ( c )
return c . JSON ( http . StatusOK , true )
}
2023-08-09 22:30:27 +08:00
// SignUp godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Sign-up to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignUp true "Sign-up object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signup request | Failed to find users"
// @Failure 401 {object} nil "signup is disabled"
// @Failure 403 {object} nil "Forbidden"
// @Failure 404 {object} nil "Not found"
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signup [POST]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) SignUp ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
signup := & SignUp { }
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( signup ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted signup request" ) . SetInternal ( err )
}
hostUserType := store . RoleHost
existedHostUsers , err := s . Store . ListUsers ( ctx , & store . FindUser {
Role : & hostUserType ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Failed to find users" ) . SetInternal ( err )
}
userCreate := & store . User {
Username : signup . Username ,
// The new signup user should be normal user by default.
Role : store . RoleUser ,
Nickname : signup . Username ,
OpenID : util . GenUUID ( ) ,
}
if len ( existedHostUsers ) == 0 {
// Change the default role to host if there is no host user.
userCreate . Role = store . RoleHost
} else {
allowSignUpSetting , err := s . Store . GetSystemSetting ( ctx , & store . FindSystemSetting {
Name : SystemSettingAllowSignUpName . String ( ) ,
} )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find system setting" ) . SetInternal ( err )
2023-04-02 09:28:02 +08:00
}
2023-08-09 21:53:06 +08:00
allowSignUpSettingValue := false
if allowSignUpSetting != nil {
err = json . Unmarshal ( [ ] byte ( allowSignUpSetting . Value ) , & allowSignUpSettingValue )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to unmarshal system setting allow signup" ) . SetInternal ( err )
}
}
if ! allowSignUpSettingValue {
return echo . NewHTTPError ( http . StatusUnauthorized , "signup is disabled" ) . SetInternal ( err )
2023-01-02 23:18:12 +08:00
}
2023-08-09 21:53:06 +08:00
}
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
passwordHash , err := bcrypt . GenerateFromPassword ( [ ] byte ( signup . Password ) , bcrypt . DefaultCost )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate password hash" ) . SetInternal ( err )
}
2022-12-28 20:58:59 +08:00
2023-08-09 21:53:06 +08:00
userCreate . PasswordHash = string ( passwordHash )
user , err := s . Store . CreateUser ( ctx , userCreate )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create user" ) . SetInternal ( err )
}
if err := GenerateTokensAndSetCookies ( c , user , s . Secret ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to generate tokens" ) . SetInternal ( err )
}
if err := s . createAuthSignUpActivity ( c , user ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create activity" ) . SetInternal ( err )
}
userMessage := convertUserFromStore ( user )
return c . JSON ( http . StatusOK , userMessage )
2022-02-03 15:32:03 +08:00
}
2023-01-01 23:55:02 +08:00
2023-07-02 14:27:23 +08:00
func ( s * APIV1Service ) createAuthSignInActivity ( c echo . Context , user * store . User ) error {
2023-01-01 23:55:02 +08:00
ctx := c . Request ( ) . Context ( )
2023-06-26 23:06:53 +08:00
payload := ActivityUserAuthSignInPayload {
2023-01-01 23:55:02 +08:00
UserID : user . ID ,
IP : echo . ExtractIPFromRealIPHeader ( ) ( c . Request ( ) ) ,
}
2023-02-17 23:55:56 +08:00
payloadBytes , err := json . Marshal ( payload )
2023-01-01 23:55:02 +08:00
if err != nil {
2023-01-02 23:18:12 +08:00
return errors . Wrap ( err , "failed to marshal activity payload" )
2023-01-01 23:55:02 +08:00
}
2023-07-06 21:56:42 +08:00
activity , err := s . Store . CreateActivity ( ctx , & store . Activity {
2023-01-01 23:55:02 +08:00
CreatorID : user . ID ,
2023-06-26 23:06:53 +08:00
Type : string ( ActivityUserAuthSignIn ) ,
Level : string ( ActivityInfo ) ,
2023-02-17 23:55:56 +08:00
Payload : string ( payloadBytes ) ,
2023-01-01 23:55:02 +08:00
} )
2023-01-07 11:49:58 +08:00
if err != nil || activity == nil {
return errors . Wrap ( err , "failed to create activity" )
}
2023-01-01 23:55:02 +08:00
return err
}
2023-01-02 23:18:12 +08:00
2023-07-02 14:27:23 +08:00
func ( s * APIV1Service ) createAuthSignUpActivity ( c echo . Context , user * store . User ) error {
2023-01-02 23:18:12 +08:00
ctx := c . Request ( ) . Context ( )
2023-06-26 23:06:53 +08:00
payload := ActivityUserAuthSignUpPayload {
2023-01-02 23:18:12 +08:00
Username : user . Username ,
IP : echo . ExtractIPFromRealIPHeader ( ) ( c . Request ( ) ) ,
}
2023-02-17 23:55:56 +08:00
payloadBytes , err := json . Marshal ( payload )
2023-01-02 23:18:12 +08:00
if err != nil {
return errors . Wrap ( err , "failed to marshal activity payload" )
}
2023-07-06 21:56:42 +08:00
activity , err := s . Store . CreateActivity ( ctx , & store . Activity {
2023-01-02 23:18:12 +08:00
CreatorID : user . ID ,
2023-06-26 23:06:53 +08:00
Type : string ( ActivityUserAuthSignUp ) ,
Level : string ( ActivityInfo ) ,
2023-02-17 23:55:56 +08:00
Payload : string ( payloadBytes ) ,
2023-01-02 23:18:12 +08:00
} )
2023-01-07 11:49:58 +08:00
if err != nil || activity == nil {
return errors . Wrap ( err , "failed to create activity" )
}
2023-01-02 23:18:12 +08:00
return err
}