diff --git a/dist/casaos-user-service-amd64_linux_amd64_v1/build/sysroot/usr/bin/casaos-user-service b/dist/casaos-user-service-amd64_linux_amd64_v1/build/sysroot/usr/bin/casaos-user-service index cf43b99..77a762e 100755 Binary files a/dist/casaos-user-service-amd64_linux_amd64_v1/build/sysroot/usr/bin/casaos-user-service and b/dist/casaos-user-service-amd64_linux_amd64_v1/build/sysroot/usr/bin/casaos-user-service differ diff --git a/dist/metadata.json b/dist/metadata.json index eb90257..b77e860 100644 --- a/dist/metadata.json +++ b/dist/metadata.json @@ -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"}} \ No newline at end of file +{"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"}} \ No newline at end of file diff --git a/route/v1.go b/route/v1.go index 879ecfe..90661db 100644 --- a/route/v1.go +++ b/route/v1.go @@ -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( diff --git a/route/v1/user.go b/route/v1/user.go index abf8b37..65e5c83 100644 --- a/route/v1/user.go +++ b/route/v1/user.go @@ -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) diff --git a/service/authentik.go b/service/authentik.go index d399703..9016fe1 100644 --- a/service/authentik.go +++ b/service/authentik.go @@ -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{} diff --git a/service/model/o_authentik_application.go b/service/model/o_authentik_application.go new file mode 100644 index 0000000..34b5223 --- /dev/null +++ b/service/model/o_authentik_application.go @@ -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"` +}