github.com/grafviktor/keep-my-secret@v0.9.10-0.20230908165355-19f35cce90e5/internal/api/auth/auth.go (about)

     1  // Package auth is used for generating access and refresh tokens,
     2  // also it has methods to verify tokens validity
     3  package auth
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/grafviktor/keep-my-secret/internal/config"
    13  
    14  	"github.com/golang-jwt/jwt/v4"
    15  )
    16  
    17  const (
    18  	// CookieName which start from '__Host-' will NOT be set if domain specified,
    19  	// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes
    20  	// Also, a cookie with such prefix, cannot be set over http connection
    21  	CookieName   = "__Host-refresh_token"
    22  	cookieSecure = true
    23  
    24  	// SameSiteStrictMode will not allow to set cookie for CORS (Cross-origin resource sharing) connections.
    25  	// siteMode     = http.SameSiteStrictMode
    26  	// Bypassing set cookie request in CORS connections. However, you also must be sure that cookie is "secure: true"
    27  	siteMode = http.SameSiteNoneMode
    28  )
    29  
    30  // Auth - struct contains all necessary information for JWT token generation
    31  type Auth struct {
    32  	// Issuer of the token. See JWT.io for more information
    33  	Issuer string
    34  	// Audience of the token. See JWT.io for more information
    35  	Audience string
    36  	// Secret of the token. Self-explanatory.
    37  	Secret string
    38  	// TokenExpiry is duration of the access token
    39  	TokenExpiry time.Duration
    40  	// RefreshExpiry is duration of the refresh token
    41  	RefreshExpiry time.Duration
    42  	// CookieDomain is domain of the cookie. Self-explanatory.
    43  	CookieDomain string
    44  	// CookiePath is path of the cookie. Self-explanatory. In our case it's always project root.
    45  	CookiePath string
    46  	//  CookieName is name of the cookie. Self-explanatory.
    47  	CookieName string
    48  }
    49  
    50  // JWTUser - struct for storing user details. Only ID for the moment
    51  type JWTUser struct {
    52  	// ID contains user login which was used during registration process
    53  	ID string `json:"id"`
    54  }
    55  
    56  // TokenPair - struct is used for marshaling tokens
    57  type TokenPair struct {
    58  	// AccessToken is a short-lived token which is used for accessing application resources.
    59  	AccessToken string `json:"access_token"`
    60  	// RefreshToken is a long-lived token which is used for querying access tokens
    61  	RefreshToken string `json:"refresh_token"`
    62  }
    63  
    64  // Claims - is utilizing default jwt claims. A subject for further extension.
    65  type Claims struct {
    66  	jwt.RegisteredClaims
    67  }
    68  
    69  // New - creates new Auth struct and with application defined values
    70  func New(ac config.AppConfig) Auth {
    71  	return Auth{
    72  		Issuer:        ac.JWTIssuer,
    73  		Audience:      ac.JWTAudience,
    74  		Secret:        ac.Secret,
    75  		TokenExpiry:   time.Minute * 10,
    76  		RefreshExpiry: time.Hour * 24,
    77  		CookieDomain:  ac.CookieDomain,
    78  		CookiePath:    "/",
    79  		CookieName:    CookieName,
    80  	}
    81  }
    82  
    83  // GenerateTokenPair - create new Refresh and Access tokens
    84  func (auth Auth) GenerateTokenPair(user *JWTUser) (TokenPair, error) { // pair for token and refresh token
    85  	// Create a token
    86  	token := jwt.New(jwt.SigningMethodHS256)
    87  
    88  	// Create a accessToken and set claims
    89  	claims := token.Claims.(jwt.MapClaims)
    90  	claims["sub"] = user.ID       // id of the user in a database
    91  	claims["aud"] = auth.Audience // audience
    92  	claims["iss"] = auth.Issuer
    93  	claims["iat"] = time.Now().UTC().Unix()                       // issued at
    94  	claims["typ"] = "JWT"                                         // type
    95  	claims["exp"] = time.Now().UTC().Add(auth.TokenExpiry).Unix() // expiry
    96  
    97  	// Create a signed token
    98  	signedAccessToken, err := token.SignedString([]byte(auth.Secret))
    99  	if err != nil {
   100  		return TokenPair{}, err
   101  	}
   102  
   103  	// Create a refreshToken and set claims
   104  	refreshToken := jwt.New(jwt.SigningMethodHS256)
   105  	refreshTokenClaims := refreshToken.Claims.(jwt.MapClaims)
   106  	refreshTokenClaims["sub"] = user.ID                                         // id of the user in a database
   107  	refreshTokenClaims["iat"] = time.Now().UTC().Unix()                         // issued at
   108  	refreshTokenClaims["exp"] = time.Now().UTC().Add(auth.RefreshExpiry).Unix() // expiry
   109  
   110  	// Create signed refresh token
   111  	signedRefreshToken, err := token.SignedString([]byte(auth.Secret))
   112  	if err != nil {
   113  		return TokenPair{}, err
   114  	}
   115  
   116  	// Create token pairs and populate with signed tokens
   117  	tokenPairs := TokenPair{
   118  		AccessToken:  signedAccessToken,
   119  		RefreshToken: signedRefreshToken,
   120  	}
   121  
   122  	// Return TokenPair
   123  	return tokenPairs, nil
   124  }
   125  
   126  // GetRefreshCookie - create new refresh cookie which contains refresh token. Used when user logs in (or register).
   127  func (auth Auth) GetRefreshCookie(refreshToken string) *http.Cookie {
   128  	return &http.Cookie{
   129  		Name:     auth.CookieName,
   130  		Path:     auth.CookiePath,
   131  		Value:    refreshToken,
   132  		Expires:  time.Now().Add(auth.RefreshExpiry),
   133  		MaxAge:   int(auth.RefreshExpiry.Seconds()),
   134  		SameSite: siteMode,
   135  		HttpOnly: true,
   136  		Secure:   cookieSecure,
   137  	}
   138  }
   139  
   140  // GetExpiredRefreshCookie - same as GetRefreshCookie,  but with expired time.  Used when user logs out.
   141  // There is way delete existing cookie from browser, thus replacing the existing one with expired.
   142  func (auth Auth) GetExpiredRefreshCookie() *http.Cookie {
   143  	return &http.Cookie{
   144  		Name:     auth.CookieName,
   145  		Path:     auth.CookiePath,
   146  		Value:    "",
   147  		Expires:  time.Unix(0, 0),
   148  		MaxAge:   -1,
   149  		SameSite: siteMode,
   150  		HttpOnly: true,
   151  		Secure:   cookieSecure,
   152  	}
   153  }
   154  
   155  // JWTVerifier - used for verifying user tokens
   156  type JWTVerifier struct{}
   157  
   158  // VerifyAuthHeader - extract users token from HTTP header and verifies it.
   159  //
   160  //nolint:lll
   161  func (t JWTVerifier) VerifyAuthHeader(config config.AppConfig, w http.ResponseWriter, r *http.Request) (string, *Claims, error) {
   162  	w.Header().Add("Vary", "Authorization")
   163  
   164  	authHeader := r.Header.Get("Authorization")
   165  
   166  	if authHeader == "" {
   167  		return "", nil, errors.New("no auth header")
   168  	}
   169  
   170  	headerParts := strings.Split(authHeader, " ") // "Bearer Token"
   171  	if len(headerParts) != 2 || headerParts[0] != "Bearer" {
   172  		return "", nil, errors.New("invalid auth header")
   173  	}
   174  
   175  	token := headerParts[1]
   176  	claims := &Claims{}
   177  	_, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) {
   178  		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
   179  			return nil, fmt.Errorf("unexpected signing method %v", token.Header["alg"])
   180  		}
   181  
   182  		return []byte(config.Secret), nil
   183  	})
   184  	if err != nil {
   185  		if strings.HasPrefix(err.Error(), "token is expired by") {
   186  			return "", nil, errors.New("expired token")
   187  		}
   188  
   189  		return "", nil, err
   190  	}
   191  
   192  	if claims.Issuer != config.JWTIssuer {
   193  		return "", nil, errors.New("invalid issuer")
   194  	}
   195  
   196  	return token, claims, nil
   197  }