diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go index 7f40f33..7464bd3 100644 --- a/pkg/models/accounts.go +++ b/pkg/models/accounts.go @@ -7,20 +7,12 @@ import ( "time" ) -type AccountState = int8 - -const ( - PendingAccountState = AccountState(iota) - ActiveAccountState -) - type Account struct { BaseModel Name string `json:"name" gorm:"uniqueIndex"` Nick string `json:"nick"` Avatar string `json:"avatar"` - State AccountState `json:"state"` Profile AccountProfile `json:"profile"` Sessions []AuthSession `json:"sessions"` Challenges []AuthChallenge `json:"challenges"` diff --git a/pkg/security/challanges.go b/pkg/security/challanges.go index 34a71ee..fd9b622 100644 --- a/pkg/security/challanges.go +++ b/pkg/security/challanges.go @@ -2,6 +2,8 @@ package security import ( "fmt" + "github.com/google/uuid" + "strings" "time" "code.smartsheep.studio/hydrogen/identity/pkg/database" @@ -83,5 +85,11 @@ func DoChallenge(challenge models.AuthChallenge, factor models.AuthFactor, code return err } + // Revoke some factor passwords + if factor.Type == models.EmailPasswordFactor { + factor.Secret = strings.ReplaceAll(uuid.NewString(), "-", "") + database.C.Save(&factor) + } + return nil } diff --git a/pkg/security/jwt.go b/pkg/security/jwt.go index 9fcc17e..1bdd05e 100644 --- a/pkg/security/jwt.go +++ b/pkg/security/jwt.go @@ -2,12 +2,16 @@ package security import ( "fmt" + "github.com/gofiber/fiber/v2" "time" "github.com/golang-jwt/jwt/v5" "github.com/spf13/viper" ) +var CookieAccessKey = "identity_auth_key" +var CookieRefreshKey = "identity_refresh_key" + type PayloadClaims struct { jwt.RegisteredClaims @@ -56,3 +60,22 @@ func DecodeJwt(str string) (PayloadClaims, error) { return claims, fmt.Errorf("unexpected token payload: not payload claims type") } } + +func SetJwtCookieSet(c *fiber.Ctx, access, refresh string) { + c.Cookie(&fiber.Cookie{ + Name: CookieAccessKey, + Value: access, + Domain: viper.GetString("security.cookie_domain"), + SameSite: viper.GetString("security.cookie_samesite"), + Expires: time.Now().Add(60 * time.Minute), + Path: "/", + }) + c.Cookie(&fiber.Cookie{ + Name: CookieRefreshKey, + Value: refresh, + Domain: viper.GetString("security.cookie_domain"), + SameSite: viper.GetString("security.cookie_samesite"), + Expires: time.Now().Add(24 * 30 * time.Hour), + Path: "/", + }) +} diff --git a/pkg/security/sessions.go b/pkg/security/sessions.go index ca62be3..12e5591 100644 --- a/pkg/security/sessions.go +++ b/pkg/security/sessions.go @@ -2,6 +2,7 @@ package security import ( "fmt" + "github.com/spf13/viper" "strconv" "time" @@ -84,15 +85,17 @@ func GetToken(session models.AuthSession) (string, string, error) { return refresh, access, err } - var err error + accessDuration := time.Duration(viper.GetInt64("security.access_token_duration")) * time.Second + refreshDuration := time.Duration(viper.GetInt64("security.refresh_token_duration")) * time.Second + var err error sub := strconv.Itoa(int(session.AccountID)) sed := strconv.Itoa(int(session.ID)) - access, err = EncodeJwt(session.AccessToken, JwtAccessType, sub, sed, session.Audiences, time.Now().Add(30*time.Minute)) + access, err = EncodeJwt(session.AccessToken, JwtAccessType, sub, sed, session.Audiences, time.Now().Add(accessDuration)) if err != nil { return refresh, access, err } - refresh, err = EncodeJwt(session.RefreshToken, JwtRefreshType, sub, sed, session.Audiences, time.Now().Add(30*24*time.Hour)) + refresh, err = EncodeJwt(session.RefreshToken, JwtRefreshType, sub, sed, session.Audiences, time.Now().Add(refreshDuration)) if err != nil { return refresh, access, err } @@ -153,5 +156,9 @@ func RefreshToken(token string) (string, string, error) { return "404", "403", err } - return GetToken(session) + if session, err := RegenSession(session); err != nil { + return "404", "403", err + } else { + return GetToken(session) + } } diff --git a/pkg/server/accounts_api.go b/pkg/server/accounts_api.go index d9fcb4a..7c199e1 100644 --- a/pkg/server/accounts_api.go +++ b/pkg/server/accounts_api.go @@ -137,12 +137,12 @@ func doRegister(c *fiber.Ctx) error { return err } else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 { return fmt.Errorf("missing magic token in request") - } - - if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil { - return err - } else { - database.C.Delete(&tk) + } else if viper.GetBool("use_registration_magic_token") { + if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil { + return err + } else { + database.C.Delete(&tk) + } } if user, err := services.CreateAccount( diff --git a/pkg/server/auth.go b/pkg/server/auth.go deleted file mode 100644 index f6def9f..0000000 --- a/pkg/server/auth.go +++ /dev/null @@ -1,36 +0,0 @@ -package server - -import ( - "code.smartsheep.studio/hydrogen/identity/pkg/security" - "code.smartsheep.studio/hydrogen/identity/pkg/services" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/keyauth" -) - -var auth = keyauth.New(keyauth.Config{ - KeyLookup: "header:Authorization", - AuthScheme: "Bearer", - Validator: func(c *fiber.Ctx, token string) (bool, error) { - claims, err := security.DecodeJwt(token) - if err != nil { - return false, err - } - - session, err := services.LookupSessionWithToken(claims.ID) - if err != nil { - return false, err - } else if err := session.IsAvailable(); err != nil { - return false, err - } - - user, err := services.GetAccount(session.AccountID) - if err != nil { - return false, err - } - - c.Locals("principal", user) - - return true, nil - }, - ContextKey: "token", -}) diff --git a/pkg/server/auth_middleware.go b/pkg/server/auth_middleware.go new file mode 100644 index 0000000..3ae13b1 --- /dev/null +++ b/pkg/server/auth_middleware.go @@ -0,0 +1,72 @@ +package server + +import ( + "code.smartsheep.studio/hydrogen/identity/pkg/security" + "code.smartsheep.studio/hydrogen/identity/pkg/services" + "fmt" + "github.com/gofiber/fiber/v2" + "strings" +) + +func authMiddleware(c *fiber.Ctx) error { + var token string + if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 { + token = cookie + } + if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 { + tk := strings.Replace(header, "Bearer", "", 1) + token = strings.TrimSpace(tk) + } + + c.Locals("token", token) + + if err := authFunc(c); err != nil { + fmt.Println(err) + return err + } + + return c.Next() +} + +func authFunc(c *fiber.Ctx, overrides ...string) error { + var token string + if len(overrides) > 0 { + token = overrides[0] + } else { + if tk, ok := c.Locals("token").(string); !ok { + return fiber.NewError(fiber.StatusUnauthorized) + } else { + token = tk + } + } + + claims, err := security.DecodeJwt(token) + if err != nil { + rtk := c.Cookies(security.CookieRefreshKey) + if len(rtk) > 0 && len(overrides) < 1 { + // Auto refresh and retry + access, refresh, err := security.RefreshToken(rtk) + if err == nil { + security.SetJwtCookieSet(c, access, refresh) + return authFunc(c, access) + } + } + return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("invalid auth key: %v", err)) + } + + session, err := services.LookupSessionWithToken(claims.ID) + if err != nil { + return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("invalid auth session: %v", err)) + } else if err := session.IsAvailable(); err != nil { + return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("unavailable auth session: %v", err)) + } + + user, err := services.GetAccount(session.AccountID) + if err != nil { + return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("invalid account: %v", err)) + } + + c.Locals("principal", user) + + return nil +} diff --git a/pkg/server/challanges_api.go b/pkg/server/challanges_api.go index 143e183..f954d70 100644 --- a/pkg/server/challanges_api.go +++ b/pkg/server/challanges_api.go @@ -127,6 +127,8 @@ func exchangeToken(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type") } + security.SetJwtCookieSet(c, access, refresh) + return c.JSON(fiber.Map{ "id_token": access, "access_token": access, diff --git a/pkg/server/startup.go b/pkg/server/startup.go index 0262fa3..2fbeaa7 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -58,18 +58,18 @@ func NewServer() { api := A.Group("/api").Name("API") { api.Get("/avatar/:avatarId", getAvatar) - api.Put("/avatar", auth, setAvatar) + api.Put("/avatar", authMiddleware, setAvatar) - api.Get("/notifications", auth, getNotifications) - api.Put("/notifications/:notificationId/read", auth, markNotificationRead) - api.Post("/notifications/subscribe", auth, addNotifySubscriber) + api.Get("/notifications", authMiddleware, getNotifications) + api.Put("/notifications/:notificationId/read", authMiddleware, markNotificationRead) + api.Post("/notifications/subscribe", authMiddleware, addNotifySubscriber) - api.Get("/users/me", auth, getUserinfo) - api.Put("/users/me", auth, editUserinfo) - api.Get("/users/me/events", auth, getEvents) - api.Get("/users/me/challenges", auth, getChallenges) - api.Get("/users/me/sessions", auth, getSessions) - api.Delete("/users/me/sessions/:sessionId", auth, killSession) + api.Get("/users/me", authMiddleware, getUserinfo) + api.Put("/users/me", authMiddleware, editUserinfo) + api.Get("/users/me/events", authMiddleware, getEvents) + api.Get("/users/me/challenges", authMiddleware, getChallenges) + api.Get("/users/me/sessions", authMiddleware, getSessions) + api.Delete("/users/me/sessions/:sessionId", authMiddleware, killSession) api.Post("/users", doRegister) api.Post("/users/me/confirm", doRegisterConfirm) @@ -79,8 +79,8 @@ func NewServer() { api.Post("/auth/token", exchangeToken) api.Post("/auth/factors/:factorId", requestFactorToken) - api.Get("/auth/o/connect", auth, preConnect) - api.Post("/auth/o/connect", auth, doConnect) + api.Get("/auth/o/connect", authMiddleware, preConnect) + api.Post("/auth/o/connect", authMiddleware, doConnect) developers := api.Group("/dev").Name("Developers API") { diff --git a/pkg/server/well_known_api.go b/pkg/server/well_known_api.go index 2bfde58..78d85b8 100644 --- a/pkg/server/well_known_api.go +++ b/pkg/server/well_known_api.go @@ -21,8 +21,8 @@ func getOidcConfiguration(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "issuer": basepath, - "authorization_endpoint": fmt.Sprintf("%s/auth/o/connect", basepath), - "token_endpoint": fmt.Sprintf("%s/api/auth/token", basepath), + "authorization_endpoint": fmt.Sprintf("%s/authMiddleware/o/connect", basepath), + "token_endpoint": fmt.Sprintf("%s/api/authMiddleware/token", basepath), "userinfo_endpoint": fmt.Sprintf("%s/api/users/me", basepath), "response_types_supported": []string{"code", "token"}, "grant_types_supported": []string{"authorization_code", "implicit", "refresh_token"}, diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go index 8f516e6..ab38ba1 100644 --- a/pkg/services/accounts.go +++ b/pkg/services/accounts.go @@ -43,9 +43,8 @@ func LookupAccount(id string) (models.Account, error) { func CreateAccount(name, nick, email, password string) (models.Account, error) { user := models.Account{ - Name: name, - Nick: nick, - State: models.PendingAccountState, + Name: name, + Nick: nick, Profile: models.AccountProfile{ Experience: 100, }, diff --git a/pkg/view/package.json b/pkg/view/package.json index d92ff33..9f79d7d 100644 --- a/pkg/view/package.json +++ b/pkg/view/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.1", "@solidjs/router": "^0.10.10", "solid-js": "^1.8.7", "universal-cookie": "^7.0.2" diff --git a/pkg/view/src/index.tsx b/pkg/view/src/index.tsx index c4af85c..010d2ce 100644 --- a/pkg/view/src/index.tsx +++ b/pkg/view/src/index.tsx @@ -8,6 +8,8 @@ import "./assets/fonts/fonts.css"; import { lazy } from "solid-js"; import { Route, Router } from "@solidjs/router"; +import "@fortawesome/fontawesome-free/css/all.min.css"; + import RootLayout from "./layouts/RootLayout.tsx"; import { UserinfoProvider } from "./stores/userinfo.tsx"; import { WellKnownProvider } from "./stores/wellKnown.tsx"; diff --git a/pkg/view/src/layouts/RootLayout.tsx b/pkg/view/src/layouts/RootLayout.tsx index 9f91deb..279672f 100644 --- a/pkg/view/src/layouts/RootLayout.tsx +++ b/pkg/view/src/layouts/RootLayout.tsx @@ -1,4 +1,4 @@ -import Navbar from "./shared/Navbar.tsx"; +import Navigatior from "./shared/Navigatior.tsx"; import { readProfiles, useUserinfo } from "../stores/userinfo.tsx"; import { createEffect, createMemo, createSignal, Show } from "solid-js"; import { readWellKnown } from "../stores/wellKnown.tsx"; @@ -52,7 +52,7 @@ export default function RootLayout(props: any) { }> - +
{props.children}
diff --git a/pkg/view/src/layouts/shared/Navbar.tsx b/pkg/view/src/layouts/shared/Navigatior.tsx similarity index 98% rename from pkg/view/src/layouts/shared/Navbar.tsx rename to pkg/view/src/layouts/shared/Navigatior.tsx index c5a7af7..30b75a3 100644 --- a/pkg/view/src/layouts/shared/Navbar.tsx +++ b/pkg/view/src/layouts/shared/Navigatior.tsx @@ -8,7 +8,7 @@ interface MenuItem { children?: MenuItem[]; } -export default function Navbar() { +export default function Navigatior() { const nav: MenuItem[] = [ { label: "You", children: [ diff --git a/pkg/view/src/pages/auth/login.tsx b/pkg/view/src/pages/auth/login.tsx index 2ffa1c3..5086d7c 100644 --- a/pkg/view/src/pages/auth/login.tsx +++ b/pkg/view/src/pages/auth/login.tsx @@ -1,7 +1,6 @@ import { readProfiles } from "../../stores/userinfo.tsx"; import { useNavigate, useSearchParams } from "@solidjs/router"; import { createSignal, For, Match, Show, Switch } from "solid-js"; -import Cookie from "universal-cookie"; export default function LoginPage() { const [title, setTitle] = createSignal("Sign in"); @@ -116,9 +115,6 @@ export default function LoginPage() { setError(err); throw new Error(err); } else { - const data = await res.json(); - new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); - new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined }); setError(null); } } diff --git a/pkg/view/src/pages/security.tsx b/pkg/view/src/pages/security.tsx index 8ebac51..4da6552 100644 --- a/pkg/view/src/pages/security.tsx +++ b/pkg/view/src/pages/security.tsx @@ -1,5 +1,5 @@ import { getAtk } from "../stores/userinfo.tsx"; -import { createSignal, For, Show } from "solid-js"; +import { createSignal, For, Match, Show, Switch } from "solid-js"; export default function DashboardPage() { const [challenges, setChallenges] = createSignal([]); @@ -12,6 +12,8 @@ export default function DashboardPage() { const [error, setError] = createSignal(null); const [submitting, setSubmitting] = createSignal(false); + const [contentTab, setContentTab] = createSignal(0); + async function readChallenges() { const res = await fetch("/api/users/me/challenges?take=10", { headers: { Authorization: `Bearer ${getAtk()}` } @@ -94,11 +96,7 @@ export default function DashboardPage() {
- - - +
Challenges
{challengeCount()}
@@ -106,11 +104,7 @@ export default function DashboardPage() {
- - - +
Sessions
{sessionCount()}
@@ -118,11 +112,7 @@ export default function DashboardPage() {
- - - +
Events
{eventCount()}
@@ -130,50 +120,52 @@ export default function DashboardPage() {
-
-
+
+
+ setContentTab(0)} /> + setContentTab(1)} /> + setContentTab(2)} /> +
+
-
- - Challenges - -
-
- - - - - - - - - - - - - {item => - - - - + + } + + +
StateIP AddressUser AgentDate
{item.id}{item.state}{item.ip_address} +
+ + +
+ + + + + + + + + + + + + {item => + + + + - - } - - -
StateIP AddressUser AgentDate
{item.id}{item.state}{item.ip_address} {item.user_agent.substring(0, 10) + "..."} - {new Date(item.created_at).toLocaleString()}
-
+
{new Date(item.created_at).toLocaleString()}
-
+ -
- - Sessions - -
+ +
@@ -192,13 +184,8 @@ export default function DashboardPage() { } @@ -206,47 +193,41 @@ export default function DashboardPage() {
{item.audiences?.join(", ")} {new Date(item.created_at).toLocaleString()} -
-
+ -
- - Events - -
-
- - - - - - - - - - - - - - {item => - - - - - + + } + + +
TypeTargetIP AddressUser AgentDate
{item.id}{item.type}{item.target}{item.ip_address} + +
+ + + + + + + + + + + + + + {item => + + + + + - - } - - -
TypeTargetIP AddressUser AgentDate
{item.id}{item.type}{item.target}{item.ip_address} {item.user_agent.substring(0, 10) + "..."} - {new Date(item.created_at).toLocaleString()}
-
+
{new Date(item.created_at).toLocaleString()}
-
- -
+ +
); diff --git a/pkg/view/src/stores/userinfo.tsx b/pkg/view/src/stores/userinfo.tsx index fd4d10f..16a87db 100644 --- a/pkg/view/src/stores/userinfo.tsx +++ b/pkg/view/src/stores/userinfo.tsx @@ -21,49 +21,23 @@ const defaultUserinfo: Userinfo = { const [userinfo, setUserinfo] = createStore(structuredClone(defaultUserinfo)); export function getAtk(): string { - return new Cookie().get("access_token"); -} - -export async function refreshAtk() { - const rtk = new Cookie().get("refresh_token"); - - const res = await fetch("/api/auth/token", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - refresh_token: rtk, - grant_type: "refresh_token" - }) - }); - if (res.status !== 200) { - console.error(await res.text()) - } else { - const data = await res.json(); - new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); - new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined }); - } + return new Cookie().get("identity_auth_key"); } function checkLoggedIn(): boolean { - return new Cookie().get("access_token"); + return new Cookie().get("identity_auth_key"); } -export async function readProfiles(recovering = true) { +export async function readProfiles() { if (!checkLoggedIn()) return; const res = await fetch("/api/users/me", { - headers: { "Authorization": `Bearer ${getAtk()}` } + credentials: "include" }); if (res.status !== 200) { - if (recovering) { - // Auto retry after refresh access token - await refreshAtk(); - return await readProfiles(false); - } else { - clearUserinfo(); - window.location.reload(); - } + clearUserinfo(); + window.location.reload(); } const data = await res.json(); @@ -77,8 +51,14 @@ export async function readProfiles(recovering = true) { } export function clearUserinfo() { - new Cookie().remove("access_token", { path: "/", maxAge: undefined }); - new Cookie().remove("refresh_token", { path: "/", maxAge: undefined }); + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i]; + const eqPos = cookie.indexOf("="); + const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } + setUserinfo(defaultUserinfo); } diff --git a/pkg/view/yarn.lock b/pkg/view/yarn.lock index 3454f61..ba9d4c1 100644 --- a/pkg/view/yarn.lock +++ b/pkg/view/yarn.lock @@ -335,6 +335,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== +"@fortawesome/fontawesome-free@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258" + integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1472,6 +1477,7 @@ source-map-js@^1.0.2: integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -1490,6 +1496,7 @@ string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== diff --git a/settings.toml b/settings.toml index 630f4ff..dc327d7 100644 --- a/settings.toml +++ b/settings.toml @@ -9,7 +9,7 @@ secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" content = "uploads" -use_registration_magic_token = true +use_registration_magic_token = false [external.firebase] credentials = "dist/firebase-certs.json" @@ -21,6 +21,12 @@ smtp_port = 465 username = "alphabot@smartsheep.studio" password = "gz937Zxxzfcd9SeH" +[security] +cookie_domain = "localhost" +cookie_samesite = "Lax" +access_token_duration = 300 +refresh_token_duration = 2592000 + [database] dsn = "host=localhost dbname=hy_identity port=5432 sslmode=disable" prefix = "identity_"