How to implement Access / Refresh Tokens

42 Views Asked by At

I have been reading up about multiple ways one can implement refresh/access token logic. I've received mixed answers on how to tackle it. My API currently requires username/password authentication Where a short life PASETO token is issued, then verified via middleware per each request.

Here is my current middleware :

    const (
      authorizationHeaderKey  = "Authorization"
      authorizationTypeBearer = "bearer"
      authorizationPayloadKey = "authorization_payload"
    )

    func authMiddleWare(token tkn.Maker) gin.HandlerFunc {
        return func(c *gin.Context) {
    
            // Get the Value of the header
            authorizationHeader := c.GetHeader(authorizationHeaderKey)
    
            //Check if a value was provided
            if len(authorizationHeader) == 0 {
                err := errors.New("authorization header is not provided")
                c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
                return
            }
    
            // Split the Header string using space delimiter (separate 'Bearer' from <token>)
            fields := strings.Fields(authorizationHeader)
    
            if len(fields) < 2 {
                err := errors.New("invalid authorization header format")
                c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
                return
            }
    
            // Check the Authorization type of the token (first portion of token)
            authorizationType := strings.ToLower(fields[0])
    
            // Check if the auth type matches type bearer
            if authorizationType != authorizationTypeBearer {
                err := fmt.Errorf("unsupported authorization type %s", authorizationType)
                c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
                return
            }
    
            accessToken := fields[1]
    
            // Tell our token maker to verify the token
            payload, err := token.VerifyToken(accessToken)
    
            if err != nil {
                if errors.Is(err, tkn.ErrExpiredToken) {
                    // if refresh token is valid and not expired, create a new access token
                    // else return 403 and force user to re authenticate
                }
                c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
                return
            }
    
            // Set the payload in the context so the next handler can access it
            c.Set(authorizationPayloadKey, payload)
            c.Next()
    
        }
    
    }

This is the implementation I am evaluating (after updating my login endpoint to return 2 tokens, one for the access and one for the refresh) :

const (
    authorizationHeaderKey   = "Authorization"
    refreshHeaderKey         = "X-Refresh-Token"
    authorizationTypeBearer  = "Bearer"
    authorizationPayloadKey  = "authorization_payload"
)

func authMiddleWare(tokenMaker tkn.Maker) gin.HandlerFunc {
    return func(c *gin.Context) {

        // Get the Value of the header
        authorizationHeader := c.GetHeader(authorizationHeaderKey)
        refreshTokenHeader := c.GetHeader(refreshHeaderKey)

        //Check if a value was provided
        if len(authorizationHeader) == 0 {
            err := errors.New("authorization header is not provided")
            c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
            return
        }

        // Split the Header string using space delimiter (separate 'Bearer' from <token>)
        fields := strings.Fields(authorizationHeader)

        if len(fields) < 2 {
            err := errors.New("invalid authorization header format")
            c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
            return
        }

        // Check the Authorization type of the token (first portion of token)
        authorizationType := strings.ToLower(fields[0])

        // Check if the auth type matches type bearer
        if authorizationType != strings.ToLower(authorizationTypeBearer) {
            err := fmt.Errorf("unsupported authorization type %s", authorizationType)
            c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
            return
        }

        accessToken := fields[1]

        // Tell our token maker to verify the token
        payload, err := tokenMaker.VerifyToken(accessToken)

        if err != nil {
            if errors.Is(err, tkn.ErrExpiredToken) {
                // if access token is expired, try to refresh it using the refresh token
                payload, err = tokenMaker.RefreshToken(refreshTokenHeader)
                if err != nil {
                    // if refresh token is invalid or not provided, return unauthorized status
                    c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
                    return
                }
                
                // generate a new access token
                newAccessToken, err := tokenMaker.CreateToken(payload.Username, addSomeDurationToCurrentTime())
                if err != nil {
                    c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
                    return
                }

                // replace the old access token with the new one
                accessToken = newAccessToken

                // optionally, set the new token in the response header
                c.Header(authorizationHeaderKey, fmt.Sprintf("%s %s", authorizationTypeBearer, newAccessToken))
            } else {
                c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
                return
            }
        }

        // Set the payload in the context so the next handler can access it
        c.Set(authorizationPayloadKey, payload)
        c.Next()
    }
}

Am I on the right track or does my flow impose security risks ?

0

There are 0 best solutions below