auth.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. package v1
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "regexp"
  8. "strings"
  9. "time"
  10. "github.com/labstack/echo/v4"
  11. "github.com/pkg/errors"
  12. "golang.org/x/crypto/bcrypt"
  13. "github.com/usememos/memos/api/auth"
  14. "github.com/usememos/memos/internal/util"
  15. "github.com/usememos/memos/plugin/idp"
  16. "github.com/usememos/memos/plugin/idp/oauth2"
  17. storepb "github.com/usememos/memos/proto/gen/store"
  18. "github.com/usememos/memos/store"
  19. )
  20. var (
  21. usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
  22. )
  23. type SignIn struct {
  24. Username string `json:"username"`
  25. Password string `json:"password"`
  26. Remember bool `json:"remember"`
  27. }
  28. type SSOSignIn struct {
  29. IdentityProviderID int32 `json:"identityProviderId"`
  30. Code string `json:"code"`
  31. RedirectURI string `json:"redirectUri"`
  32. }
  33. type SignUp struct {
  34. Username string `json:"username"`
  35. Password string `json:"password"`
  36. }
  37. func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
  38. g.POST("/auth/signin", s.SignIn)
  39. g.POST("/auth/signin/sso", s.SignInSSO)
  40. g.POST("/auth/signout", s.SignOut)
  41. g.POST("/auth/signup", s.SignUp)
  42. }
  43. // SignIn godoc
  44. //
  45. // @Summary Sign-in to memos.
  46. // @Tags auth
  47. // @Accept json
  48. // @Produce json
  49. // @Param body body SignIn true "Sign-in object"
  50. // @Success 200 {object} store.User "User information"
  51. // @Failure 400 {object} nil "Malformatted signin request"
  52. // @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
  53. // @Failure 403 {object} nil "User has been archived with username %s"
  54. // @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"
  55. // @Router /api/v1/auth/signin [POST]
  56. func (s *APIV1Service) SignIn(c echo.Context) error {
  57. ctx := c.Request().Context()
  58. signin := &SignIn{}
  59. disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
  60. Name: SystemSettingDisablePasswordLoginName.String(),
  61. })
  62. if err != nil {
  63. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  64. }
  65. if disablePasswordLoginSystemSetting != nil {
  66. disablePasswordLogin := false
  67. err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin)
  68. if err != nil {
  69. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
  70. }
  71. if disablePasswordLogin {
  72. return echo.NewHTTPError(http.StatusUnauthorized, "Password login is deactivated")
  73. }
  74. }
  75. if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
  76. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
  77. }
  78. user, err := s.Store.GetUser(ctx, &store.FindUser{
  79. Username: &signin.Username,
  80. })
  81. if err != nil {
  82. return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
  83. }
  84. if user == nil {
  85. return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
  86. } else if user.RowStatus == store.Archived {
  87. return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
  88. }
  89. // Compare the stored hashed password, with the hashed version of the password that was received.
  90. if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
  91. // If the two passwords don't match, return a 401 status.
  92. return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
  93. }
  94. var expireAt time.Time
  95. if !signin.Remember {
  96. expireAt = time.Now().Add(auth.AccessTokenDuration)
  97. }
  98. accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret))
  99. if err != nil {
  100. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
  101. }
  102. if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
  103. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
  104. }
  105. cookieExp := time.Now().Add(auth.CookieExpDuration)
  106. setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
  107. userMessage := convertUserFromStore(user)
  108. return c.JSON(http.StatusOK, userMessage)
  109. }
  110. // SignInSSO godoc
  111. //
  112. // @Summary Sign-in to memos using SSO.
  113. // @Tags auth
  114. // @Accept json
  115. // @Produce json
  116. // @Param body body SSOSignIn true "SSO sign-in object"
  117. // @Success 200 {object} store.User "User information"
  118. // @Failure 400 {object} nil "Malformatted signin request"
  119. // @Failure 401 {object} nil "Access denied, identifier does not match the filter."
  120. // @Failure 403 {object} nil "User has been archived with username {username}"
  121. // @Failure 404 {object} nil "Identity provider not found"
  122. // @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"
  123. // @Router /api/v1/auth/signin/sso [POST]
  124. func (s *APIV1Service) SignInSSO(c echo.Context) error {
  125. ctx := c.Request().Context()
  126. signin := &SSOSignIn{}
  127. if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
  128. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
  129. }
  130. identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
  131. ID: &signin.IdentityProviderID,
  132. })
  133. if err != nil {
  134. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider").SetInternal(err)
  135. }
  136. if identityProvider == nil {
  137. return echo.NewHTTPError(http.StatusNotFound, "Identity provider not found")
  138. }
  139. var userInfo *idp.IdentityProviderUserInfo
  140. if identityProvider.Type == store.IdentityProviderOAuth2Type {
  141. oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.OAuth2Config)
  142. if err != nil {
  143. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create identity provider instance").SetInternal(err)
  144. }
  145. token, err := oauth2IdentityProvider.ExchangeToken(ctx, signin.RedirectURI, signin.Code)
  146. if err != nil {
  147. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to exchange token").SetInternal(err)
  148. }
  149. userInfo, err = oauth2IdentityProvider.UserInfo(token)
  150. if err != nil {
  151. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user info").SetInternal(err)
  152. }
  153. }
  154. identifierFilter := identityProvider.IdentifierFilter
  155. if identifierFilter != "" {
  156. identifierFilterRegex, err := regexp.Compile(identifierFilter)
  157. if err != nil {
  158. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile identifier filter").SetInternal(err)
  159. }
  160. if !identifierFilterRegex.MatchString(userInfo.Identifier) {
  161. return echo.NewHTTPError(http.StatusUnauthorized, "Access denied, identifier does not match the filter.").SetInternal(err)
  162. }
  163. }
  164. user, err := s.Store.GetUser(ctx, &store.FindUser{
  165. Username: &userInfo.Identifier,
  166. })
  167. if err != nil {
  168. return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
  169. }
  170. if user == nil {
  171. allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
  172. Name: SystemSettingAllowSignUpName.String(),
  173. })
  174. if err != nil {
  175. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  176. }
  177. allowSignUpSettingValue := false
  178. if allowSignUpSetting != nil {
  179. err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
  180. if err != nil {
  181. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
  182. }
  183. }
  184. if !allowSignUpSettingValue {
  185. return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
  186. }
  187. userCreate := &store.User{
  188. Username: userInfo.Identifier,
  189. // The new signup user should be normal user by default.
  190. Role: store.RoleUser,
  191. Nickname: userInfo.DisplayName,
  192. Email: userInfo.Email,
  193. }
  194. password, err := util.RandomString(20)
  195. if err != nil {
  196. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate random password").SetInternal(err)
  197. }
  198. passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
  199. if err != nil {
  200. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
  201. }
  202. userCreate.PasswordHash = string(passwordHash)
  203. user, err = s.Store.CreateUser(ctx, userCreate)
  204. if err != nil {
  205. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
  206. }
  207. }
  208. if user.RowStatus == store.Archived {
  209. return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
  210. }
  211. accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
  212. if err != nil {
  213. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
  214. }
  215. if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
  216. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
  217. }
  218. cookieExp := time.Now().Add(auth.CookieExpDuration)
  219. setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
  220. userMessage := convertUserFromStore(user)
  221. return c.JSON(http.StatusOK, userMessage)
  222. }
  223. // SignOut godoc
  224. //
  225. // @Summary Sign-out from memos.
  226. // @Tags auth
  227. // @Produce json
  228. // @Success 200 {boolean} true "Sign-out success"
  229. // @Router /api/v1/auth/signout [POST]
  230. func (s *APIV1Service) SignOut(c echo.Context) error {
  231. ctx := c.Request().Context()
  232. accessToken := findAccessToken(c)
  233. userID, _ := getUserIDFromAccessToken(accessToken, s.Secret)
  234. userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
  235. // Auto remove the current access token from the user access tokens.
  236. if err == nil && len(userAccessTokens) != 0 {
  237. accessTokens := []*storepb.AccessTokensUserSetting_AccessToken{}
  238. for _, userAccessToken := range userAccessTokens {
  239. if accessToken != userAccessToken.AccessToken {
  240. accessTokens = append(accessTokens, userAccessToken)
  241. }
  242. }
  243. if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{
  244. UserId: userID,
  245. Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
  246. Value: &storepb.UserSetting_AccessTokens{
  247. AccessTokens: &storepb.AccessTokensUserSetting{
  248. AccessTokens: accessTokens,
  249. },
  250. },
  251. }); err != nil {
  252. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
  253. }
  254. }
  255. removeAccessTokenAndCookies(c)
  256. return c.JSON(http.StatusOK, true)
  257. }
  258. // SignUp godoc
  259. //
  260. // @Summary Sign-up to memos.
  261. // @Tags auth
  262. // @Accept json
  263. // @Produce json
  264. // @Param body body SignUp true "Sign-up object"
  265. // @Success 200 {object} store.User "User information"
  266. // @Failure 400 {object} nil "Malformatted signup request | Failed to find users"
  267. // @Failure 401 {object} nil "signup is disabled"
  268. // @Failure 403 {object} nil "Forbidden"
  269. // @Failure 404 {object} nil "Not found"
  270. // @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"
  271. // @Router /api/v1/auth/signup [POST]
  272. func (s *APIV1Service) SignUp(c echo.Context) error {
  273. ctx := c.Request().Context()
  274. signup := &SignUp{}
  275. if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
  276. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
  277. }
  278. hostUserType := store.RoleHost
  279. existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
  280. Role: &hostUserType,
  281. })
  282. if err != nil {
  283. return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
  284. }
  285. if !usernameMatcher.MatchString(strings.ToLower(signup.Username)) {
  286. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
  287. }
  288. userCreate := &store.User{
  289. Username: signup.Username,
  290. // The new signup user should be normal user by default.
  291. Role: store.RoleUser,
  292. Nickname: signup.Username,
  293. }
  294. if len(existedHostUsers) == 0 {
  295. // Change the default role to host if there is no host user.
  296. userCreate.Role = store.RoleHost
  297. } else {
  298. allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
  299. Name: SystemSettingAllowSignUpName.String(),
  300. })
  301. if err != nil {
  302. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  303. }
  304. allowSignUpSettingValue := false
  305. if allowSignUpSetting != nil {
  306. err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
  307. if err != nil {
  308. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
  309. }
  310. }
  311. if !allowSignUpSettingValue {
  312. return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
  313. }
  314. }
  315. passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
  316. if err != nil {
  317. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
  318. }
  319. userCreate.PasswordHash = string(passwordHash)
  320. user, err := s.Store.CreateUser(ctx, userCreate)
  321. if err != nil {
  322. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
  323. }
  324. accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
  325. if err != nil {
  326. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
  327. }
  328. if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
  329. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
  330. }
  331. cookieExp := time.Now().Add(auth.CookieExpDuration)
  332. setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
  333. userMessage := convertUserFromStore(user)
  334. return c.JSON(http.StatusOK, userMessage)
  335. }
  336. func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
  337. userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
  338. if err != nil {
  339. return errors.Wrap(err, "failed to get user access tokens")
  340. }
  341. userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
  342. AccessToken: accessToken,
  343. Description: "Account sign in",
  344. }
  345. userAccessTokens = append(userAccessTokens, &userAccessToken)
  346. if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{
  347. UserId: user.ID,
  348. Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
  349. Value: &storepb.UserSetting_AccessTokens{
  350. AccessTokens: &storepb.AccessTokensUserSetting{
  351. AccessTokens: userAccessTokens,
  352. },
  353. },
  354. }); err != nil {
  355. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
  356. }
  357. return nil
  358. }
  359. // removeAccessTokenAndCookies removes the jwt token from the cookies.
  360. func removeAccessTokenAndCookies(c echo.Context) {
  361. cookieExp := time.Now().Add(-1 * time.Hour)
  362. setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
  363. }
  364. // setTokenCookie sets the token to the cookie.
  365. func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
  366. cookie := new(http.Cookie)
  367. cookie.Name = name
  368. cookie.Value = token
  369. cookie.Expires = expiration
  370. cookie.Path = "/"
  371. // Http-only helps mitigate the risk of client side script accessing the protected cookie.
  372. cookie.HttpOnly = true
  373. cookie.SameSite = http.SameSiteStrictMode
  374. c.SetCookie(cookie)
  375. }