OAuth 2.0 и JWT авторизация на Go: от теории к практике
Современные веб-приложения и API редко обходятся без надежной системы аутентификации и авторизации. Два стандарта — OAuth 2.0 и JWT (JSON Web Tokens) — стали де-факто стандартами в мире разработки. В этой статье мы на практических примерах разберем, как реализовать OAuth 2.0 авторизацию с использованием JWT на языке Go (Golang).
Мы создадим простой REST API сервер, который будет выдавать JWT токены после успешной аутентификации через OAuth 2.0 провайдера (например, Google) и проверять их при каждом запросе.
1. Основы OAuth 2.0 и JWT
Прежде чем писать код, кратко разберемся с терминами.
- OAuth 2.0 — протокол авторизации, который позволяет пользователям предоставлять третьим сторонам ограниченный доступ к своим ресурсам без передачи логина и пароля. Основные роли: Resource Owner (пользователь), Client (наше приложение), Authorization Server (сервер, выдающий токены), Resource Server (сервер с защищенными данными).
- JWT — компактный URL-безопасный формат токена, который состоит из трех частей: header, payload и signature. JWT может содержать любые данные (claims) и подписывается секретным ключом или парой ключей RSA.
В нашей реализации OAuth 2.0 будет использоваться для получения identity токена от провайдера (Google), а затем мы сгенерируем собственный JWT для дальнейшей работы с нашим API.
2. Настройка проекта и зависимости
Создадим новый проект на Go и установим необходимые пакеты:
mkdir go-oauth-jwt
cd go-oauth-jwt
go mod init example.com/go-oauth-jwt
go get github.com/gin-gonic/gin
go get github.com/golang-jwt/jwt/v5
go get golang.org/x/oauth2
go get golang.org/x/oauth2/google
Мы будем использовать:
- Gin — быстрый HTTP фреймворк для Go.
- golang-jwt — библиотека для работы с JWT.
- oauth2 — стандартная библиотека Go для OAuth 2.0.
Также вам потребуется зарегистрировать приложение в Google Cloud Console, создать OAuth 2.0 Client ID и Secret, и указать Redirect URI (например, http://localhost:8080/auth/google/callback).
3. Реализация OAuth 2.0 логина через Google
Начнем с создания конфигурации OAuth 2.0 и эндпоинтов для входа и обратного вызова.
package main
import (
"crypto/rand"
"encoding/base64"
"net/http"
"os"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
var (
googleOauthConfig *oauth2.Config
oauthStateString string
)
func init() {
googleOauthConfig = &oauth2.Config{
RedirectURL: "http://localhost:8080/auth/google/callback",
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: google.Endpoint,
}
// Генерируем случайный state токен для защиты от CSRF
b := make([]byte, 32)
rand.Read(b)
oauthStateString = base64.StdEncoding.EncodeToString(b)
}
func main() {
r := gin.Default()
r.GET("/auth/google/login", handleGoogleLogin)
r.GET("/auth/google/callback", handleGoogleCallback)
r.Run(":8080")
}
func handleGoogleLogin(c *gin.Context) {
url := googleOauthConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url)
}
func handleGoogleCallback(c *gin.Context) {
state := c.Query("state")
if state != oauthStateString {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid oauth state"})
return
}
code := c.Query("code")
token, err := googleOauthConfig.Exchange(c, code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to exchange token"})
return
}
// Здесь мы получаем информацию о пользователе
client := googleOauthConfig.Client(c, token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get user info"})
return
}
defer resp.Body.Close()
// Декодируем ответ (упрощенно)
// В реальном проекте используйте encoding/json
c.JSON(http.StatusOK, gin.H{
"message": "Login successful",
"token": token.AccessToken,
})
}
Этот код реализует начальный этап OAuth 2.0: перенаправление пользователя на страницу входа Google и обработку обратного вызова с получением access token.
4. Генерация JWT токена после аутентификации
Теперь, когда у нас есть информация о пользователе, мы сгенерируем собственный JWT токен. Это позволит нашему API не зависеть от Google токена.
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
Email string `json:"email"`
Name string `json:"name"`
jwt.RegisteredClaims
}
var jwtKey = []byte("your-secret-key") // В продакшене используйте безопасное хранилище
func generateJWT(email, name string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
Email: email,
Name: name,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "go-oauth-jwt",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}
Обновим handleGoogleCallback, чтобы после получения данных пользователя генерировать JWT:
// В handleGoogleCallback после получения userinfo
// Предположим, мы получили email и name
email := "user@example.com" // Извлекаем из ответа Google
name := "John Doe"
jwtToken, err := generateJWT(email, name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Login successful",
"jwt_token": jwtToken,
})