Files
test_deploy/internal/auth/keycloak/auth.go
Alex Shevchuk d84487d238 1
2025-08-18 17:12:04 +03:00

391 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package keycloak
import (
"context"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth"
"github.com/Nerzal/gocloak/v13"
)
func (k *Client) RegisterUser(ctx context.Context, request auth.RegisterUserRequest) (*auth.RegisterUserResponse, error) {
attrs := make(map[string][]string)
if request.User.Patronymic != nil {
attrs[PatronymicAttr] = []string{*request.User.Patronymic}
}
for key, value := range request.User.Permissions {
attrs[key] = []string{value}
}
for _, perm := range permissionsSlice {
if _, ok := attrs[perm]; !ok {
return nil, errors.Join(ErrBadRequest, ErrMissingPermission)
}
}
client, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
if err != nil {
return nil, k.handleError(err)
}
userGroup, err := k.getUserGroupByType(request.UserType)
if err != nil {
return nil, err
}
emailVerified := false
if k.env != "production" {
emailVerified = true
}
userId, err := k.client.CreateUser(
ctx,
client.AccessToken,
k.realm,
gocloak.User{
Email: gocloak.StringP(request.User.Email),
EmailVerified: &emailVerified,
FirstName: gocloak.StringP(request.User.FirstName),
LastName: gocloak.StringP(request.User.SecondName),
Groups: &[]string{userGroup},
Enabled: gocloak.BoolP(true),
Attributes: &attrs,
})
if err != nil {
return nil, k.handleError(err)
}
if err = k.client.SetPassword(
ctx, client.AccessToken,
userId, k.realm,
request.User.Password, false,
); err != nil {
return nil, k.handleError(err)
}
return &auth.RegisterUserResponse{
UserId: userId,
}, nil
}
func (k *Client) getUserGroupByType(userType int32) (string, error) {
switch userType {
case UserAgentType:
return UserGroupAgents, nil
case UserDistributorType:
return UserGroupDistributor, nil
default:
return "", ErrUnknownUserType
}
}
func (k *Client) LoginUser(ctx context.Context, request auth.LoginUserRequest) (*auth.LoginUserResponse, error) {
token, err := k.client.Login(ctx, k.clientId, k.clientSecret, k.realm, request.Email, request.Password)
if err != nil {
return nil, k.handleError(err)
}
userInfo, err := k.jwtManager.GetUserInfoFromToken(token.AccessToken)
if err != nil {
return nil, k.handleError(err)
}
return &auth.LoginUserResponse{
UserId: userInfo.UserId,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (k *Client) GetNewAccessToken(ctx context.Context, request auth.GetNewAccessTokenRequest) (*auth.GetNewAccessTokenResponse, error) {
token, err := k.client.RefreshToken(ctx, request.RefreshToken, k.clientId, k.clientSecret, k.realm)
if err != nil {
return nil, k.handleError(err)
}
return &auth.GetNewAccessTokenResponse{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (k *Client) CheckTokenIsValid(ctx context.Context, token string) (bool, error) {
resp, err := k.client.RetrospectToken(ctx, token, k.clientId, k.clientSecret, k.realm)
if err != nil {
return false, k.handleError(err)
}
if resp.Active == nil {
return false, nil
}
return *resp.Active, nil
}
func (k *Client) LogoutUser(ctx context.Context, request auth.LogoutUserRequest) error {
if err := k.client.Logout(ctx, k.clientId, k.clientSecret, k.realm, request.RefreshToken); err != nil {
return k.handleError(err)
}
if err := k.client.RevokeToken(ctx, k.realm, k.clientId, k.clientSecret, request.RefreshToken); err != nil {
return k.handleError(err)
}
return nil
}
func (k *Client) GetUserInfo(ctx context.Context, id string) (*auth.UserInfo, error) {
client, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
if err != nil {
return nil, k.handleError(err)
}
user, err := k.client.GetUserByID(ctx, client.AccessToken, k.realm, id)
if err != nil {
return nil, k.handleError(err)
}
if user.Attributes == nil {
return nil, ErrNoAttributes
}
groups, err := k.client.GetUserGroups(ctx, client.AccessToken, k.realm, id, gocloak.GetGroupsParams{})
if err != nil {
return nil, k.handleError(err)
}
if len(groups) == 0 {
return nil, ErrUnknownUserType
}
if groups[0].Name == nil {
return nil, ErrUnknownUserType
}
userType, err := k.getUserTypeByUserGroup(*groups[0].Name)
if err != nil {
return nil, err
}
return &auth.UserInfo{
Email: *user.Email,
FirstName: *user.FirstName,
SecondName: *user.LastName,
Patronymic: k.getUserPatronymicFromAttributes(*user.Attributes),
Permissions: k.getUserPermissionsFromAttributes(*user.Attributes),
UserType: userType,
}, nil
}
func (k *Client) getUserPatronymicFromAttributes(attr attributes) *string {
item, ok := attr[PatronymicAttr]
if !ok || len(item) == 0 {
return nil
}
return &item[0]
}
func (k *Client) getUserPermissionsFromAttributes(attr attributes) permissions {
resp := make(map[string]string)
for _, permission := range permissionsSlice {
if item, ok := attr[permission]; ok {
if len(item) != 0 {
resp[permission] = item[0]
} else {
resp[permission] = "" // todo: подумать над тем как правильно делать в таких ситуациях
}
} else {
resp[permission] = "" // todo: подумать над тем как правильно делать в таких ситуациях
}
}
return resp
}
func (k *Client) getUserTypeByUserGroup(attr string) (int32, error) {
switch attr {
case UserGroupAgents:
return UserAgentType, nil
case UserGroupDistributor:
return UserDistributorType, nil
default:
return -1, ErrUnknownUserType
}
}
func (k *Client) GetPermissionsByUsersId(ctx context.Context, id string) (*auth.GetPermissionsByUsersIdResponse, error) {
userInfo, err := k.GetUserInfo(ctx, id)
if err != nil {
return nil, err
}
return &auth.GetPermissionsByUsersIdResponse{
Balance: userInfo.Permissions[PermissionBalance],
Company: userInfo.Permissions[PermissionCompany],
Employees: userInfo.Permissions[PermissionEmployees],
Profile: userInfo.Permissions[PermissionProfile],
Submissions: userInfo.Permissions[PermissionSubmissions],
Vacancies: userInfo.Permissions[PermissionVacancies],
}, nil
}
func (k *Client) CheckPermissions(ctx context.Context, request auth.CheckPermissionsRequest) (bool, error) {
userInfo, err := k.GetUserInfo(ctx, request.UserId)
if err != nil {
return false, err
}
if userInfo.Permissions == nil {
return false, ErrInternal
}
if val, ok := userInfo.Permissions[request.RequiredPermission]; ok {
return k.getPermissionLevel(val) >= k.getPermissionLevel(request.RequiredPermissionLevel), nil
}
return false, ErrInternal
}
func (k *Client) getPermissionLevel(permission string) int8 {
switch permission {
case PermissionLevelCanEdit:
return 2
case PermissionLevelCanView:
return 1
case PermissionLevelNoPermission:
return 0
default:
return -1
}
}
func (k *Client) VerifyEmail(ctx context.Context, userID string, token string) error {
if err := k.jwtManager.VerifyEmailToken(token, userID); err != nil {
return err
}
// Get admin token
adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
if err != nil {
return k.handleError(err)
}
// Get user
user, err := k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, userID)
if err != nil {
return k.handleError(err)
}
// Set email as verified
user.EmailVerified = gocloak.BoolP(true)
// Update user
if err := k.client.UpdateUser(ctx, adminToken.AccessToken, k.realm, *user); err != nil {
return k.handleError(err)
}
return nil
}
func (k *Client) GetEmailVerificationStatus(ctx context.Context, userID string) (*auth.GetEmailVerificationResponse, error) {
// Get admin token
adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
if err != nil {
return nil, k.handleError(err)
}
// Get user
user, err := k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, userID)
if err != nil {
return nil, k.handleError(err)
}
// Check if email is verified
verified := false
if user.EmailVerified != nil {
verified = *user.EmailVerified
}
return &auth.GetEmailVerificationResponse{
EmailVerified: verified,
}, nil
}
func (k *Client) GetUserEmailVerificationToken(ctx context.Context, uid string) (*auth.GetUserTokenResponse, error) {
// Получение токена администратора
adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
if err != nil {
return nil, k.handleError(err)
}
// Получаем пользователя, чтобы убедиться в его существовании
_, err = k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, uid)
if err != nil {
return nil, k.handleError(err)
}
// Создаём кастомный JWT-токен для верификации email сроком на 24 часа
claims := jwt.MapClaims{
"sub": uid, // Subject (user ID)
"iat": time.Now().Unix(), // Issued at
"exp": time.Now().Add(24 * time.Hour).Unix(), // Expires in 24 hours
"purpose": "email_verification", // Custom claim to identify purpose
"iss": "molva-api-gateway", // Issuer
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Используем клиентский секрет как ключ подписи
signedToken, err := token.SignedString([]byte(k.clientSecret))
if err != nil {
return nil, ErrInternal
}
return &auth.GetUserTokenResponse{
AccessToken: signedToken,
}, nil
}
func (k *Client) ResetPassword(ctx context.Context, request auth.ResetPasswordRequest) error {
token, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
if err != nil {
return k.handleError(err)
}
user, err := k.client.GetUsers(
ctx,
token.AccessToken,
k.realm,
gocloak.GetUsersParams{
Username: gocloak.StringP(request.Email),
},
)
if err != nil {
return k.handleError(err)
}
if len(user) == 0 {
return ErrNotFound
}
if err := k.client.SetPassword(
ctx,
token.AccessToken,
*user[0].ID,
k.realm,
request.NewPassword,
false,
); err != nil {
return k.handleError(err)
}
return nil
}