Skip to content

Commit

Permalink
feat: add totp support
Browse files Browse the repository at this point in the history
Signed-off-by: Zixuan Liu <nodeces@gmail.com>
  • Loading branch information
nodece committed Mar 13, 2022
1 parent 971df48 commit cbc6116
Show file tree
Hide file tree
Showing 22 changed files with 824 additions and 580 deletions.
6 changes: 3 additions & 3 deletions authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ p, *, *, GET, /.well-known/openid-configuration, *, *
p, *, *, *, /api/certs, *, *
p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/totp, *, *
p, *, *, POST, /api/totp, *, *
p, *, *, POST, /api/delete-totp, *, *
p, *, *, POST, /api/two-factor/setup/totp/init, *, *
p, *, *, POST, /api/two-factor/setup/totp/verity, *, *
p, *, *, POST, /api/two-factor/auth/totp, *, *
`

sa := stringadapter.NewAdapter(ruleText)
Expand Down
66 changes: 54 additions & 12 deletions controllers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,37 @@ import (
"github.com/casdoor/casdoor/util"
)

func codeToResponse(code *object.Code) *Response {
if code.Code == "" {
return &Response{Status: "error", Msg: code.Message, Data: code.Code}
}
const TwoFactorSessionKey = "TwoFactor"
const NextTwoFactor = "nextTwoFactor"

type TwoFactorSessionData struct {
UserId string
EnableSession bool
AutoSignIn bool
}

return &Response{Status: "ok", Msg: "", Data: code.Code}
func (c *ApiController) SetFactorSessionData(data *TwoFactorSessionData) {
c.SetSession(TwoFactorSessionKey, data)
}

func (c *ApiController) GetTOTPSessionData() *TwoFactorSessionData {
v := c.GetSession(TwoFactorSessionKey)
data, ok := v.(*TwoFactorSessionData)
if !ok {
return nil
}
return data
}

// HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
userId := user.GetId()
if form.Type == ResponseTypeLogin {
if user.IsEnableTwoFactor() {
c.SetFactorSessionData(&TwoFactorSessionData{UserId: userId, EnableSession: true, AutoSignIn: form.AutoSignin})
resp = &Response{Status: NextTwoFactor}
return
}
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
resp = &Response{Status: "ok", Msg: "", Data: userId}
Expand All @@ -60,28 +79,51 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
return
}
code := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge)
resp = codeToResponse(code)
status := ""
if code.Code == "" {
status = "error"
} else {
if user.IsEnableTwoFactor() {
status = NextTwoFactor
} else {
status = "ok"
}
}
resp = &Response{Status: status, Msg: code.Message, Data: code.Code}

if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
if user.IsEnableTwoFactor() {
c.SetFactorSessionData(&TwoFactorSessionData{
UserId: userId,
EnableSession: true,
AutoSignIn: form.AutoSignin,
})
return
} else {
c.SetSessionUsername(userId)
}
}
} else {
resp = &Response{Status: "error", Msg: fmt.Sprintf("Unknown response type: %s", form.Type)}
}

// if user did not check auto signin
if resp.Status == "ok" && !form.AutoSignin {
timestamp := time.Now().Unix()
timestamp += 3600 * 24
c.SetSessionData(&SessionData{
ExpireTime: timestamp,
})
c.setExpireForSession()
}

return resp
}

func (c *ApiController) setExpireForSession() {
timestamp := time.Now().Unix()
timestamp += 3600 * 24
c.SetSessionData(&SessionData{
ExpireTime: timestamp,
})
}

// GetApplicationLogin ...
// @Title GetApplicationLogin
// @Tag Login API
Expand Down
60 changes: 0 additions & 60 deletions controllers/totp.go

This file was deleted.

164 changes: 164 additions & 0 deletions controllers/two_factor_totp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"fmt"
"net/http"
"strings"

"github.com/astaxie/beego"
jsoniter "github.com/json-iterator/go"

"github.com/casdoor/casdoor/object"
"github.com/google/uuid"
)

type TOTPInit struct {
Secret string `json:"secret"`
RecoveryCode string `json:"recoveryCode"`
URL string `json:"url"`
}

// TwoFactorSetupInitTOTP
// @Title: Setup init TOTP
// @Tag: Two-Factor API
// @router: /two-factor/setup/totp/init [post]
func (c *ApiController) TwoFactorSetupInitTOTP() {
userId := jsoniter.Get(c.Ctx.Input.RequestBody, "userId").ToString()
if len(userId) == 0 {
c.ResponseError(http.StatusText(http.StatusBadRequest))
return
}

user := object.GetUser(userId)
if user == nil {
c.ResponseError("User doesn't exist")
return
}
if len(user.TotpSecret) != 0 {
c.ResponseError("User has TOTP two-factor authentication enabled")
return
}

application := object.GetApplicationByUser(user)

issuer := beego.AppConfig.String("appname")
accountName := fmt.Sprintf("%s/%s", application.Name, user.Name)

key, err := object.NewTOTPKey(issuer, accountName)
if err != nil {
c.ResponseError(err.Error())
return
}

recoveryCode, err := uuid.NewRandom()
if err != nil {
c.ResponseError(err.Error())
return
}

resp := TOTPInit{
Secret: key.Secret(),
RecoveryCode: strings.ReplaceAll(recoveryCode.String(), "-", ""),
URL: key.URL(),
}
c.ResponseOk(resp)
}

// TwoFactorSetupVerityTOTP
// @Title: Setup verity TOTP
// @Tag: Two-Factor API
// @router: /two-factor/setup/totp/verity [post]
func (c *ApiController) TwoFactorSetupVerityTOTP() {
secret := jsoniter.Get(c.Ctx.Input.RequestBody, "secret").ToString()
passcode := jsoniter.Get(c.Ctx.Input.RequestBody, "passcode").ToString()
ok := object.ValidateTOTPPassCode(passcode, secret)
if ok {
c.ResponseOk(http.StatusText(http.StatusOK))
} else {
c.ResponseError(http.StatusText(http.StatusUnauthorized))
}
}

// TwoFactorEnableTOTP
// @Title: Enable TOTP
// @Tag: Two-Factor API
// @router: /two-factor/totp [post]
func (c *ApiController) TwoFactorEnableTOTP() {
userId := jsoniter.Get(c.Ctx.Input.RequestBody, "userId").ToString()
secret := jsoniter.Get(c.Ctx.Input.RequestBody, "secret").ToString()
recoveryCode := jsoniter.Get(c.Ctx.Input.RequestBody, "recoveryCode").ToString()

user := object.GetUser(userId)
if user == nil {
c.ResponseError("User doesn't exist")
return
}

object.SetUserField(user, "totp_secret", secret)
object.SetUserField(user, "two_factor_recovery_code", recoveryCode)

c.ResponseOk(http.StatusText(http.StatusOK))
}

// TwoFactorRemoveTOTP
// @Title: Remove TOTP
// @Tag: Two-Factor API
// @router: /two-factor/totp [delete]
func (c *ApiController) TwoFactorRemoveTOTP() {
userId := jsoniter.Get(c.Ctx.Input.RequestBody, "userId").ToString()

user := object.GetUser(userId)
if user == nil {
c.ResponseError("User doesn't exist")
return
}

object.SetUserField(user, "totp_secret", "")
c.ResponseOk(http.StatusText(http.StatusOK))
}

// TwoFactorAuthTOTP
// @Title: Auth TOTP
// @Tag: TOTP API
// @router: /two-factor/auth/totp [post]
func (c *ApiController) TwoFactorAuthTOTP() {
totpSessionData := c.GetTOTPSessionData()
if totpSessionData == nil {
c.ResponseError(http.StatusText(http.StatusBadRequest))
return
}

user := object.GetUser(totpSessionData.UserId)
if user == nil {
c.ResponseError("User does not exist")
return
}

passcode := jsoniter.Get(c.Ctx.Input.RequestBody, "passcode").ToString()
ok := object.ValidateTOTPPassCode(passcode, user.TotpSecret)
if ok {
if totpSessionData.EnableSession {
c.SetSessionUsername(totpSessionData.UserId)
}
if !totpSessionData.AutoSignIn {
c.setExpireForSession()
}
c.ResponseOk(http.StatusText(http.StatusOK))
} else {
c.ResponseError(http.StatusText(http.StatusUnauthorized))
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.2.0
github.com/google/uuid v1.3.0
github.com/jinzhu/configor v1.2.1 // indirect
github.com/json-iterator/go v1.1.11
github.com/markbates/goth v1.69.0
github.com/pquerna/otp v1.3.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
Expand Down
1 change: 0 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package main

import (
"flag"

"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
_ "github.com/astaxie/beego/session/redis"
Expand Down
2 changes: 0 additions & 2 deletions object/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ type Application struct {
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
TotpPeriod int `json:"totpPeriod"`
TotpSecretSize int `json:"totpSecretSize"`
ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"`
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
Expand Down

0 comments on commit cbc6116

Please sign in to comment.