Use golang to encapsulate and implement scan code login
Tags: github, go
Published: 2023-10-28 09:08:15

# Use go to encapsulate and implement code scanning login

I will talk about the implementation and usage of token-go scan code login.

Github: https://github.com/weloe/token-go

## Scan code login process

1. Open the login page, display a QR code, and poll the QR code status (web)

2. After opening the APP and scanning the QR code, the APP displays confirmation and cancel buttons (app)

3. The login page displays the scanned user avatar and other information (web)

4. The user clicks on the APP to confirm login (app)

5. The login page learns from the polling QR code status that the user has confirmed login and obtains the login credentials (web)

6. The page login is successful and enters the main application page (web)

We can know that the logged in QR code has the following states:

1. Waiting for scanning

2. Scanned code, waiting for user confirmation

3. The QR code has been scanned and the user agrees to authorize

4. The QR code has been scanned and the user has canceled the authorization.

5. expired

And our code-scanning client (usually a mobile app) can modify the status of the QR code.

1. Confirm that the code has been scanned

2. Agree to authorize

3. Cancel authorization

## Implementation ideas

What we encapsulate is mainly the state maintenance of QR codes, not including the generation of QR codes. The generation of QR codes is left to the user.

The commonly used methods for QR code status are as follows.

```go
// QRCode api

CreateQRCodeState(QRCodeId string, timeout int64) error

GetQRCodeTimeout(QRCodeId string) int64

GetQRCode(QRCodeId string) *model.QRCode

GetQRCodeState(QRCodeId string) model.QRCodeState

Scanned(QRCodeId string, loginId string) (string, error)

ConfirmAuth(QRCodeTempToken string) error

CancelAuth(QRCodeTempToken string) error

```

QRCodeId is used by us as the unique identifier of the QR code status.

When creating a QR code, we need to pass in the QRCodeId and timeout to set the timeout of the QR code. After all, the QR code cannot be used permanently.

Of course, the prerequisite for confirming that the code has been scanned is that you are logged in, so we use loginId as a parameter to bind it to QRCodeId.

For authorization and cancellation of authorization, we use the temporary Token returned by the api to confirm the scan code to perform operations.

The storage and acquisition of information are obtained using the Adapter inside the framework.

 

## Code

### QR code status and information

First we need to set the QR code status

Waiting to scan code——1

Scanned code, waiting for user confirmation——2

The QR code has been scanned and the user agrees to authorize——3

The QR code has been scanned and the user has canceled authorization——4

Expired - 5

package model 

 type QRCodeState int 

```go
package model

type QRCodeState int

// QRCode State
const (
	WaitScan    QRCodeState = 1
	WaitAuth    QRCodeState = 2
	ConfirmAuth QRCodeState = 3
	CancelAuth  QRCodeState = 4
	Expired     QRCodeState = 5
)
```

The information needed to maintain the QR code, that is, the unique id of the QR code, the current status of the QR code, and the unique id of the user for whom the QR code belongs.

```go
package model

type QRCodeState int

// QRCode State
const (
	WaitScan    QRCodeState = 1
	WaitAuth    QRCodeState = 2
	ConfirmAuth QRCodeState = 3
	CancelAuth  QRCodeState = 4
	Expired     QRCodeState = 5
)
```



### Initialize QR code status

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L229

Before scanning the QR code in the APP, we must first create a QR code status and set it to WaitScan, which is 1. To create QR code information, we use the Adapter interface inside our framework to store it.

```go
func (e *Enforcer) CreateQRCodeState(QRCodeId string, timeout int64) error {
	return e.createQRCode(QRCodeId, timeout)
}
```

```go
func (e *Enforcer) createQRCode(id string, timeout int64) error {
	return e.adapter.Set(e.spliceQRCodeKey(id), model.NewQRCode(id), timeout)
}
```

e.spliceQRCodeKey is a splicing method for stored keys.

### Get the remaining time of the QR code

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L319

Use our Adapter to get it through QRCodeId

```go
func (e *Enforcer) GetQRCodeTimeout(QRCodeId string) int64 {
    return e.getQRCodeTimeout(QRCodeId)
}
```

```go
func (e *Enforcer) getQRCodeTimeout(id string) int64 {
	return e.adapter.GetTimeout(e.spliceQRCodeKey(id))
}
```

### Get QR code information

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L301

Use Adapter to get

```go
func (e *Enforcer) GetQRCode(QRCodeId string) *model.QRCode {
	return e.getQRCode(QRCodeId)
}
```

### Get QR code status

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L311

Also use Adapter to obtain

```go
// GetQRCodeState
//	WaitScan   = 1
//	WaitAuth   = 2
//	ConfirmAuth  = 3
//	CancelAuth = 4
//	Expired    = 5
func (e *Enforcer) GetQRCodeState(QRCodeId string) model.QRCodeState {
	qrCode := e.getQRCode(QRCodeId)
	if qrCode == nil {
		return model.Expired
	}
	return qrCode.State
}
```

### Delete QR code information

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L323

```go
func (e *Enforcer) DeleteQRCode(QRCodeId string) error {
	return e.deleteQRCode(QRCodeId)
}
```

```go
func (e *Enforcer) deleteQRCode(id string) error {
	return e.adapter.Delete(e.spliceQRCodeKey(id))
}
```

### Confirm scan code

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L234

To confirm the code scanning, you must first determine whether the QR code exists, and then verify whether the status of the QR code is WaitScan, which is 1. After verification, bind the user's unique loginId, and finally create a temporary token with a value of QRCodeId and return it. This temporary token is used for authorization and cancellation of authorization.

```go
// Scanned update state to constant.WaitAuth, return tempToken
func (e *Enforcer) Scanned(QRCodeId string, loginId string) (string, error) {
	qrCode := e.getQRCode(QRCodeId)
	if qrCode == nil {
		return "", fmt.Errorf("QRCode doesn't exist")
	}
	if qrCode.State != model.WaitScan {
		return "", fmt.Errorf("QRCode state error: unexpected state value %v, want is %v", qrCode.State, model.WaitScan)
	}
	qrCode.State = model.WaitAuth
	qrCode.LoginId = loginId

	err := e.updateQRCode(QRCodeId, qrCode)
	if err != nil {
		return "", err
	}
	tempToken, err := e.CreateTempToken(e.config.TokenStyle, "qrCode", QRCodeId, e.config.Timeout)
	if err != nil {
		return "", err
	}
	return tempToken, nil
}
```

### Agree to authorize

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L257

To agree to authorization, we need to use the temporary token returned when we confirm the scan code. First, we need to verify the temporary token. The ParseTempToken method is an interface for verifying the temporary token and obtaining the value corresponding to the token. After verifying the token, obtain the QRCodeId, and then verify the status corresponding to the QRCodeId. It should be WaitAuth waiting for authorization, which is 2. The last step is to change the QR code status to ConfirmAuth which is 3. Of course, don't forget to delete the temporary token.

```go
// ConfirmAuth update state to constant.ConfirmAuth
func (e *Enforcer) ConfirmAuth(tempToken string) error {
	qrCodeId := e.ParseTempToken("qrCode", tempToken)
	if qrCodeId == "" {
		return fmt.Errorf("confirm failed, tempToken error: %v", tempToken)
	}
	qrCode, err := e.getAndCheckQRCodeState(qrCodeId, model.WaitAuth)
	if err != nil {
		return err
	}

	qrCode.State = model.ConfirmAuth
	err = e.updateQRCode(qrCodeId, qrCode)
	if err != nil {
		return err
	}
	err = e.DeleteTempToken("qrCode", tempToken)
	if err != nil {
		return err
	}
	return err
}

```

### Cancel authorization

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L280

To cancel authorization, we also need to use the temporary token returned when we confirm the scan code. First, we need to verify the temporary token. The ParseTempToken method is the method to verify the temporary token. Through this method, we can obtain the QRCodeId value corresponding to the token. After verifying the token, obtain the QRCodeId, and then verify the status corresponding to the QRCodeId. It should be WaitAuth waiting for authorization, which is 2. The last step is to change the QR code status to CancelAuth which is 4. Also don't forget to delete the temporary token.

```go
// CancelAuth update state to constant.CancelAuth
func (e *Enforcer) CancelAuth(tempToken string) error {
	qrCodeId := e.ParseTempToken("qrCode", tempToken)
	if qrCodeId == "" {
		return fmt.Errorf("confirm failed, tempToken error: %v", tempToken)
	}
	qrCode, err := e.getAndCheckQRCodeState(qrCodeId, model.WaitAuth)
	if err != nil {
		return err
	}
	qrCode.State = model.CancelAuth
	err = e.updateQRCode(qrCodeId, qrCode)
	if err != nil {
		return err
	}
	err = e.DeleteTempToken("qrCode", tempToken)
	if err != nil {
		return err
	}
	return err
}
```

 

## test

```go
func TestEnforcer_ConfirmQRCode(t *testing.T) {
	enforcer, _ := NewTestEnforcer(t)
	// in APP
	loginId := "1"
	token, err := enforcer.LoginById(loginId)
	if err != nil {
		t.Fatalf("Login failed: %v", err)
	}
	t.Logf("login token: %v", token)

	qrCodeId := "q1"

	err = enforcer.CreateQRCodeState(qrCodeId, -1)
	if err != nil {
		t.Fatalf("CreateQRCodeState() failed: %v", err)
	}
	t.Logf("After CreateQRCodeState(), current QRCode state: %v", enforcer.GetQRCodeState(qrCodeId))
	loginIdByToken, err := enforcer.GetLoginIdByToken(token)
	if err != nil {
		t.Fatalf("GetLoginIdByToken() failed: %v", err)
	}
	tempToken, err := enforcer.Scanned(qrCodeId, loginIdByToken)
	if err != nil {
		t.Fatalf("Scanned() failed: %v", err)
	}
	if state := enforcer.GetQRCodeState(qrCodeId); state != model.WaitAuth {
		t.Fatalf("After Scanned(), QRCode should be %v", model.WaitAuth)
	}
	t.Logf("After Scanned(), current QRCode state: %v", enforcer.GetQRCodeState(qrCodeId))
	t.Logf("tempToken: %v", tempToken)
	err = enforcer.ConfirmAuth(tempToken)
	if err != nil {
		t.Fatalf("ConfirmAuth() failed: %v", err)
	}
	if state := enforcer.GetQRCodeState(qrCodeId); state != model.ConfirmAuth {
		t.Fatalf("After ConfirmAuth(), QRCode should be %v", model.ConfirmAuth)
	}
	t.Logf("After ConfirmAuth(), current QRCode state: %v", enforcer.GetQRCodeState(qrCodeId))
	if enforcer.GetQRCodeState(qrCodeId) == model.ConfirmAuth {
		loginId := enforcer.getQRCode(qrCodeId).LoginId
		t.Logf("id: [%v] QRCode login successfully.", loginId)
	}
}
```

## how to use

https://github.com/weloe/token-go/blob/master/examples/qrcode/qrcode-server.go

Install token-go, `go get github.com/weloe/token-go`

```go
package main

import (
	"fmt"
	tokenGo "github.com/weloe/token-go"
	"github.com/weloe/token-go/model"
	"log"
	"net/http"
)

var enforcer *tokenGo.Enforcer

func main() {
	var err error
	// use default adapter
	adapter := tokenGo.NewDefaultAdapter()
	enforcer, err = tokenGo.NewEnforcer(adapter)
	// enable logger
	enforcer.EnableLog()
	if err != nil {
		log.Fatal(err)
	}

	http.HandleFunc("/qrcode/create", create)
	http.HandleFunc("/qrcode/scanned", scanned)
	http.HandleFunc("/qrcode/confirmAuth", confirmAuth)
	http.HandleFunc("/qrcode/cancelAuth", cancelAuth)
	http.HandleFunc("/qrcode/getState", getState)

	log.Fatal(http.ListenAndServe(":8081", nil))
}

func create(w http.ResponseWriter, request *http.Request) {
	// you should implement generate QR code method, returns QRCodeId to CreateQRCodeState
	// called generate QR code, returns QRCodeId to CreateQRCodeState
	//
	QRCodeId := "generatedQRCodeId"
	err := enforcer.CreateQRCodeState(QRCodeId, 50000)
	if err != nil {
		fmt.Fprintf(w, "CreateQRCodeState() failed: %v", err)
		return
	}
	fmt.Fprintf(w, "QRCodeId = %v", QRCodeId)
}

func scanned(w http.ResponseWriter, req *http.Request) {
	loginId, err := enforcer.GetLoginId(tokenGo.NewHttpContext(req, w))
	if err != nil {
		fmt.Fprintf(w, "GetLoginId() failed: %v", err)
		return
	}
	QRCodeId := req.URL.Query().Get("QRCodeId")
	tempToken, err := enforcer.Scanned(QRCodeId, loginId)
	if err != nil {
		fmt.Fprintf(w, "Scanned() failed: %v", err)
		return
	}
	fmt.Fprintf(w, "tempToken = %v", tempToken)
}
func getState(w http.ResponseWriter, req *http.Request) {
	QRCodeId := req.URL.Query().Get("QRCodeId")
	state := enforcer.GetQRCodeState(QRCodeId)
	if state == model.ConfirmAuth {
		qrCode := enforcer.GetQRCode(QRCodeId)
		if qrCode == nil {
			fmt.Fprintf(w, "login error. state = %v, code is nil", state)
			return
		}
		loginId := qrCode.LoginId
		token, err := enforcer.LoginById(loginId)
		if err != nil {
			fmt.Fprintf(w, "Login error: %s\n", err)
		}
		fmt.Fprintf(w, "%v login success. state = %v, token = %v", loginId, state, token)
		return
	} else if state == model.CancelAuth {
		fmt.Fprintf(w, "QRCodeId be cancelled: %v", QRCodeId)
		return
	}
	fmt.Fprintf(w, "state = %v", state)
}

func cancelAuth(w http.ResponseWriter, req *http.Request) {
	tempToken := req.URL.Query().Get("tempToken")
	err := enforcer.CancelAuth(tempToken)
	if err != nil {
		fmt.Fprintf(w, "CancelAuth() failed: %v", err)
		return
	}
	fmt.Fprint(w, "ConfirmAuth() success")
}

func confirmAuth(w http.ResponseWriter, req *http.Request) {
	tempToken := req.URL.Query().Get("tempToken")
	err := enforcer.ConfirmAuth(tempToken)
	if err != nil {
		fmt.Fprintf(w, "ConfirmAuth() failed: %v", err)
		return
	}
	fmt.Fprint(w, "ConfirmAuth() success")
}
```

You can also know from the initial process and testing methods

`/qrcode/create `with the parameter QR code id . The backend calls the method of generating the QR code (which needs to be implemented by yourself). Then call the `enforcer.CreateQRCodeState() `method to initialize the QR code state.

Scan the QR code from the APP and request the backend `/qrcode/scanned` . The backend first checks the token sent from the APP (use the` enforcer.isLoginByToken() `method of the framework to judge) whether it is in the login state, use enforcer. `GetLoginId() `obtains the corresponding loginId, and then calls the `enforcer.Scanned()` method. Then return the temporary token.

After receiving the temporary token, the APP chooses to agree or cancel the authorization, that is, the temporary token is sent to the backend `/qrcode/confirmAuth` or `/qrcode/cancelAuth` , and the backend calls the `enforcer.ConfirmAuth() `or `enforcer.CancelAuth() `method to agree or cancel. Authorization.

After initializing the QR code status, the Web side must continue to request the backend` /qrcode/getState `. The backend calls the `GetQRCodeState` method to obtain the QR code status. If the QR code status is timed out, that is, Expired, the front end will delete the QR code information. It prompts that the QR code has expired. Regenerate the QR code. If the obtained status is equal to the confirmation authorization ConfirmAuth, perform the login operation `enforcer.LoginById() `and return the login credential token.