2023-06-17 21:25:46 +08:00
package v1
2022-02-03 15:32:03 +08:00
import (
2023-09-14 22:57:27 +08:00
"context"
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"
2023-09-18 22:37:13 +08:00
"strings"
2023-09-14 20:16:17 +08:00
"time"
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-09-17 22:55:13 +08:00
"golang.org/x/crypto/bcrypt"
2023-09-14 20:16:17 +08:00
"github.com/usememos/memos/api/auth"
2023-10-26 09:02:50 +08:00
"github.com/usememos/memos/internal/util"
2023-02-19 09:50:30 +08:00
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/plugin/idp/oauth2"
2023-09-14 22:57:27 +08:00
storepb "github.com/usememos/memos/proto/gen/store"
2023-02-19 09:50:30 +08:00
"github.com/usememos/memos/store"
2022-02-03 15:32:03 +08:00
)
2023-09-18 22:34:31 +08:00
var (
2023-10-14 13:42:27 +08:00
usernameMatcher = regexp . MustCompile ( "^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$" )
2023-09-18 22:34:31 +08:00
)
2023-06-17 21:25:46 +08:00
type SignIn struct {
Username string ` json:"username" `
Password string ` json:"password" `
2023-10-19 09:38:49 +08:00
Remember bool ` json:"remember" `
2023-06-17 21:25:46 +08:00
}
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-10-19 09:38:49 +08:00
var expireAt time . Time
2023-11-13 13:52:04 +08:00
// Set cookie expiration to 100 years to make it persistent.
cookieExp := time . Now ( ) . AddDate ( 100 , 0 , 0 )
2023-10-19 09:38:49 +08:00
if ! signin . Remember {
expireAt = time . Now ( ) . Add ( auth . AccessTokenDuration )
2023-11-13 13:52:04 +08:00
cookieExp = time . Now ( ) . Add ( auth . CookieExpDuration )
2023-10-19 09:38:49 +08:00
}
accessToken , err := auth . GenerateAccessToken ( user . Username , user . ID , expireAt , [ ] byte ( s . Secret ) )
2023-09-14 20:16:17 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to generate tokens, err: %s" , err ) ) . SetInternal ( err )
2023-08-09 21:53:06 +08:00
}
2023-09-14 22:57:27 +08:00
if err := s . UpsertAccessTokenToStore ( ctx , user , accessToken ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to upsert access token, err: %s" , err ) ) . SetInternal ( err )
}
2023-09-14 20:16:17 +08:00
setTokenCookie ( c , auth . AccessTokenCookieName , accessToken , cookieExp )
2023-08-09 21:53:06 +08:00
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 )
}
2023-11-18 12:51:07 +08:00
allowSignUpSettingValue := true
2023-08-25 23:10:51 +08:00
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-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 ) )
}
2023-09-20 20:48:34 +08:00
accessToken , err := auth . GenerateAccessToken ( user . Username , user . ID , time . Now ( ) . Add ( auth . AccessTokenDuration ) , [ ] byte ( s . Secret ) )
2023-09-14 20:16:17 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to generate tokens, err: %s" , err ) ) . SetInternal ( err )
2023-08-09 21:53:06 +08:00
}
2023-09-14 22:57:27 +08:00
if err := s . UpsertAccessTokenToStore ( ctx , user , accessToken ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to upsert access token, err: %s" , err ) ) . SetInternal ( err )
}
2023-09-14 20:16:17 +08:00
cookieExp := time . Now ( ) . Add ( auth . CookieExpDuration )
setTokenCookie ( c , auth . AccessTokenCookieName , accessToken , cookieExp )
2023-08-09 21:53:06 +08:00
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-09-20 19:24:26 +08:00
func ( s * APIV1Service ) SignOut ( c echo . Context ) error {
accessToken := findAccessToken ( c )
userID , _ := getUserIDFromAccessToken ( accessToken , s . Secret )
2023-11-22 23:20:45 +08:00
err := removeAccessTokenAndCookies ( c , s . Store , userID , accessToken )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to remove access token, err: %s" , err ) ) . SetInternal ( err )
2023-09-20 19:24:26 +08:00
}
2023-08-09 21:53:06 +08:00
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 )
}
2023-09-18 22:37:13 +08:00
if ! usernameMatcher . MatchString ( strings . ToLower ( signup . Username ) ) {
2023-09-18 22:34:31 +08:00
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "Invalid username %s" , signup . Username ) ) . SetInternal ( err )
}
2023-08-09 21:53:06 +08:00
userCreate := & store . User {
Username : signup . Username ,
// The new signup user should be normal user by default.
Role : store . RoleUser ,
Nickname : signup . Username ,
}
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
2023-11-18 12:51:07 +08:00
allowSignUpSettingValue := true
2023-08-09 21:53:06 +08:00
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 )
}
2023-09-20 20:48:34 +08:00
accessToken , err := auth . GenerateAccessToken ( user . Username , user . ID , time . Now ( ) . Add ( auth . AccessTokenDuration ) , [ ] byte ( s . Secret ) )
2023-09-14 20:16:17 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to generate tokens, err: %s" , err ) ) . SetInternal ( err )
2023-08-09 21:53:06 +08:00
}
2023-09-14 22:57:27 +08:00
if err := s . UpsertAccessTokenToStore ( ctx , user , accessToken ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to upsert access token, err: %s" , err ) ) . SetInternal ( err )
}
2023-09-14 20:16:17 +08:00
cookieExp := time . Now ( ) . Add ( auth . CookieExpDuration )
setTokenCookie ( c , auth . AccessTokenCookieName , accessToken , cookieExp )
2023-08-09 21:53:06 +08:00
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-09-14 22:57:27 +08:00
func ( s * APIV1Service ) UpsertAccessTokenToStore ( ctx context . Context , user * store . User , accessToken string ) error {
userAccessTokens , err := s . Store . GetUserAccessTokens ( ctx , user . ID )
if err != nil {
return errors . Wrap ( err , "failed to get user access tokens" )
}
userAccessToken := storepb . AccessTokensUserSetting_AccessToken {
AccessToken : accessToken ,
Description : "Account sign in" ,
}
userAccessTokens = append ( userAccessTokens , & userAccessToken )
if _ , err := s . Store . UpsertUserSettingV1 ( ctx , & storepb . UserSetting {
UserId : user . ID ,
Key : storepb . UserSettingKey_USER_SETTING_ACCESS_TOKENS ,
Value : & storepb . UserSetting_AccessTokens {
AccessTokens : & storepb . AccessTokensUserSetting {
AccessTokens : userAccessTokens ,
} ,
} ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , fmt . Sprintf ( "failed to upsert user setting, err: %s" , err ) ) . SetInternal ( err )
}
return nil
}
2023-09-20 19:24:26 +08:00
// removeAccessTokenAndCookies removes the jwt token from the cookies.
2023-11-22 23:20:45 +08:00
func removeAccessTokenAndCookies ( c echo . Context , s * store . Store , userID int32 , token string ) error {
err := s . RemoveUserAccessToken ( c . Request ( ) . Context ( ) , userID , token )
if err != nil {
return err
}
2023-09-14 20:16:17 +08:00
cookieExp := time . Now ( ) . Add ( - 1 * time . Hour )
setTokenCookie ( c , auth . AccessTokenCookieName , "" , cookieExp )
2023-11-22 23:20:45 +08:00
return nil
2023-09-14 20:16:17 +08:00
}
// setTokenCookie sets the token to the cookie.
func setTokenCookie ( c echo . Context , name , token string , expiration time . Time ) {
cookie := new ( http . Cookie )
cookie . Name = name
cookie . Value = token
cookie . Expires = expiration
cookie . Path = "/"
// Http-only helps mitigate the risk of client side script accessing the protected cookie.
cookie . HttpOnly = true
cookie . SameSite = http . SameSiteStrictMode
c . SetCookie ( cookie )
}