update code (integrate with authentik)

This commit is contained in:
KaySar12 2024-08-14 10:00:32 +07:00
parent c385748979
commit 74786ca6c8
6 changed files with 209 additions and 54 deletions

2
dist/metadata.json vendored
View File

@ -1 +1 @@
{"project_name":"casaos-user-service","tag":"v1.0.0","previous_tag":"","version":"1.0.1","commit":"634c492519a2c929fc20b8d2d1f2f403ea79197c","date":"2024-08-13T11:38:58.760480343+07:00","runtime":{"goos":"linux","goarch":"amd64"}}
{"project_name":"casaos-user-service","tag":"v1.0.0","previous_tag":"","version":"1.0.1","commit":"c385748979d44c704f123d60348b8273856a0d2c","date":"2024-08-13T16:27:44.744967057+07:00","runtime":{"goos":"linux","goarch":"amd64"}}

View File

@ -37,6 +37,7 @@ func InitRouter() *gin.Engine {
r.GET("/v1/users/status", v1.GetUserStatus) // init/check
r.POST("/v1/users/oidc/login", v1.OIDCLogin)
r.GET("/v1/users/oidc/callback", v1.OIDCCallback)
r.GET("/v1/users/oidc/profile", v1.OIDCProfile)
v1Group := r.Group("/v1")
v1Group.Use(jwt.JWT(

View File

@ -7,7 +7,6 @@ import (
"encoding/base64"
"encoding/json"
json2 "encoding/json"
"fmt"
"image"
"image/png"
"io"
@ -221,6 +220,8 @@ func OIDCLogin(c *gin.Context) {
func OIDCCallback(c *gin.Context) {
w := c.Writer
r := c.Request
// Verify state cookie
state, err := r.Cookie("state")
if err != nil {
http.Error(w, "state not found", http.StatusBadRequest)
@ -230,47 +231,110 @@ func OIDCCallback(c *gin.Context) {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
// Exchange authorization code for token
oauth2Token, err := oauth2Config.Exchange(context.Background(), r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
userInfo, err := providerOIDC.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token))
if err != nil {
http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
return
}
resp := struct {
OAuth2Token *oauth2.Token
UserInfo *oidc.UserInfo
}{oauth2Token, userInfo}
// data, err := json.MarshalIndent(resp, "", " ")
// if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// return
// }
//Save Userinfo and access token logic
service.MyService.Authentik().GetUserInfo(resp.OAuth2Token.AccessToken)
fmt.Println(resp)
oldUser := service.MyService.User().GetUserInfoByUserName(resp.UserInfo.Email)
if oldUser.Id > 0 {
service.MyService.User().UpdateUser(oldUser)
} else {
user := model2.UserDBModel{}
user.Username = resp.UserInfo.Email
user.Password = encryption.GetMD5ByStr("123")
user.Role = "admin"
user = service.MyService.User().CreateUser(user)
if user.Id == 0 {
c.JSON(common_err.SERVICE_ERROR, model.Result{Success: common_err.SERVICE_ERROR, Message: common_err.GetMsg(common_err.SERVICE_ERROR)})
return
}
}
expiryDuration := time.Until(oauth2Token.Expiry)
c.SetCookie("accessToken", oauth2Token.AccessToken, int(expiryDuration.Seconds()), "/", "", false, true)
c.SetCookie("refreshToken", oauth2Token.RefreshToken, int(expiryDuration.Seconds()), "/", "", false, true)
c.Redirect(http.StatusFound, state.Value)
}
func OIDCProfile(c *gin.Context) {
json := make(map[string]string)
c.ShouldBind(&json)
w := c.Writer
accessToken, err := c.Cookie("accessToken")
if err != nil {
c.Redirect(http.StatusFound, "/#/oidc")
}
// r := c.Request
// Get Authentik user info
authentikUser, err := service.MyService.Authentik().GetUserInfo(accessToken, baseURL)
if err != nil {
http.Error(w, "Failed to get Authentik user info: "+err.Error(), http.StatusInternalServerError)
return
}
// Handle user data in local database
user := service.MyService.User().GetUserInfoByUserName(authentikUser.User.Username)
if user.Id > 0 {
// Update existing user
user.Nickname = authentikUser.User.Username
user.Email = authentikUser.User.Email
user.Role = determineUserRole(authentikUser.User.IsSuperuser)
user.Avatar = authentikUser.User.Avatar
service.MyService.User().UpdateUser(user)
} else {
// Create new user
user = model2.UserDBModel{
Username: authentikUser.User.Username,
Password: hashPassword(),
Role: determineUserRole(authentikUser.User.IsSuperuser),
Avatar: authentikUser.User.Avatar,
}
user = service.MyService.User().CreateUser(user)
if user.Id == 0 {
c.JSON(http.StatusInternalServerError, model.Result{Success: common_err.SERVICE_ERROR, Message: common_err.GetMsg(common_err.SERVICE_ERROR)})
return
}
}
// Generate tokens
token, err := generateTokens(user)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Result{Success: common_err.SERVICE_ERROR, Message: err.Error()})
return
}
data := make(map[string]interface{}, 2)
data["token"] = token
data["user"] = user
c.JSON(common_err.SUCCESS,
model.Result{
Success: common_err.SUCCESS,
Message: common_err.GetMsg(common_err.SUCCESS),
Data: data,
})
}
func determineUserRole(isSuperuser bool) string {
if isSuperuser {
return "admin"
}
return "user"
}
func hashPassword() string {
generatePassword, err := randString(16)
if err != nil {
return ""
}
return encryption.GetMD5ByStr(generatePassword)
}
func generateTokens(user model2.UserDBModel) (system_model.VerifyInformation, error) {
privateKey, _ := service.MyService.User().GetKeyPair()
accessToken, err := jwt.GetAccessToken(user.Username, privateKey, user.Id)
if err != nil {
return system_model.VerifyInformation{}, err
}
refreshToken, err := jwt.GetRefreshToken(user.Username, privateKey, user.Id)
if err != nil {
return system_model.VerifyInformation{}, err
}
return system_model.VerifyInformation{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: time.Now().Add(3 * time.Hour).Unix(),
}, nil
}
func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
c := &http.Cookie{
Name: name,
@ -324,7 +388,6 @@ func PostOMVLogin(c *gin.Context) {
log.Printf("Error getting user: %v", err)
return // or handle it in a way that fits your application's error handling strategy
}
var userData model2.OMVUser
err = json2.Unmarshal([]byte(getUser), &userData)

View File

@ -1,9 +1,8 @@
package service
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
@ -11,38 +10,88 @@ import (
)
type AuthentikService interface {
HelloWorld() string
GetUserInfo(accessToken string) model2.AuthentikUser
GetUserInfo(accessToken string, baseURL string) (model2.AuthentikUser, error)
GetUserApp(accessToken string, baseURL string) (model2.AuthentikApplication, error)
}
type authentikService struct {
}
func (a *authentikService) GetUserInfo(accessToken string) model2.AuthentikUser {
var (
APICorePrefix = "/api/v3/core"
)
func (a *authentikService) GetUserApp(accessToken string, baseURL string) (model2.AuthentikApplication, error) {
bearer := "Bearer " + accessToken
req, err := http.NewRequest("GET", "", bytes.NewBuffer(nil))
path := baseURL + APICorePrefix + "/applications/"
req, err := http.NewRequest("GET", path, nil)
if err != nil {
return model2.AuthentikApplication{}, err
}
req.Header.Set("Authorization", bearer)
req.Header.Add("Accept", "application/json")
client := &http.Client{}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
for key, val := range via[0].Header {
req.Header[key] = val
}
return err
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Always follow redirects
return nil
},
}
resp, err := client.Do(req)
if err != nil {
log.Println("Error on response.\n[ERRO] -", err)
} else {
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Println(string(data))
log.Println("Error on request:", err)
return model2.AuthentikApplication{}, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Println("HTTP error:", resp.Status)
return model2.AuthentikApplication{}, fmt.Errorf("HTTP error: %s", resp.Status)
}
return model2.AuthentikUser{}
var app model2.AuthentikApplication
if err := json.NewDecoder(resp.Body).Decode(&app); err != nil {
log.Println("Error decoding response:", err)
return model2.AuthentikApplication{}, err
}
return app, nil
}
func (a *authentikService) HelloWorld() string {
return "Hello World!"
func (a *authentikService) GetUserInfo(accessToken string, baseURL string) (model2.AuthentikUser, error) {
bearer := "Bearer " + accessToken
path := baseURL + APICorePrefix + "/users/me/"
req, err := http.NewRequest("GET", path, nil) // No need for bytes.NewBuffer(nil) for GET requests without a body
if err != nil {
return model2.AuthentikUser{}, err
}
req.Header.Set("Authorization", bearer)
req.Header.Add("Accept", "application/json")
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Always follow redirects
return nil
},
}
resp, err := client.Do(req)
if err != nil {
log.Println("Error on request:", err)
return model2.AuthentikUser{}, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Println("HTTP error:", resp.Status)
return model2.AuthentikUser{}, fmt.Errorf("HTTP error: %s", resp.Status)
}
var user model2.AuthentikUser
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
log.Println("Error decoding response:", err)
return model2.AuthentikUser{}, err
}
return user, nil
}
func NewAuthentikService() AuthentikService {
return &authentikService{}

View File

@ -0,0 +1,42 @@
package model
type AuthentikApplication struct {
Pagination struct {
Count int64 `json:"count"`
Current int64 `json:"current"`
EndIndex int64 `json:"end_index"`
Next int64 `json:"next"`
Previous int64 `json:"previous"`
StartIndex int64 `json:"start_index"`
TotalPages int64 `json:"total_pages"`
} `json:"pagination"`
Results []struct {
BackchannelProviders []interface{} `json:"backchannel_providers"`
BackchannelProvidersObj []interface{} `json:"backchannel_providers_obj"`
Group string `json:"group"`
LaunchURL string `json:"launch_url"`
MetaDescription string `json:"meta_description"`
MetaIcon string `json:"meta_icon"`
MetaLaunchURL string `json:"meta_launch_url"`
MetaPublisher string `json:"meta_publisher"`
Name string `json:"name"`
OpenInNewTab bool `json:"open_in_new_tab"`
Pk string `json:"pk"`
PolicyEngineMode string `json:"policy_engine_mode"`
Provider int64 `json:"provider"`
ProviderObj struct {
AssignedApplicationName string `json:"assigned_application_name"`
AssignedApplicationSlug string `json:"assigned_application_slug"`
AuthenticationFlow string `json:"authentication_flow"`
AuthorizationFlow string `json:"authorization_flow"`
Component string `json:"component"`
MetaModelName string `json:"meta_model_name"`
Name string `json:"name"`
Pk int64 `json:"pk"`
PropertyMappings []string `json:"property_mappings"`
VerboseName string `json:"verbose_name"`
VerboseNamePlural string `json:"verbose_name_plural"`
} `json:"provider_obj"`
Slug string `json:"slug"`
} `json:"results"`
}