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 }