✨ OAuth
This commit is contained in:
parent
8e315642a4
commit
c25a1f5c82
13
.idea/workspace.xml
generated
13
.idea/workspace.xml
generated
@ -4,17 +4,15 @@
|
|||||||
<option name="autoReloadType" value="ALL" />
|
<option name="autoReloadType" value="ALL" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: User center page">
|
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Personalize">
|
||||||
<change afterPath="$PROJECT_DIR$/pkg/server/ui/personalize.go" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/server/ui/oauth.go" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/pkg/views/users/personalize.gohtml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/views/authorize.gohtml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.en.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.en.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.en.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.en.json" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/ui/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/index.go" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/server/ui/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/index.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/views/layouts/user-center.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/layouts/user-center.gohtml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/views/layouts/user-center.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/layouts/user-center.gohtml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/views/signup.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/signup.gohtml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/views/users/personalize.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/users/personalize.gohtml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/views/users/me.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/users/me.gohtml" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@ -138,7 +136,8 @@
|
|||||||
<MESSAGE value=":sparkles: Sign up & Sign in" />
|
<MESSAGE value=":sparkles: Sign up & Sign in" />
|
||||||
<MESSAGE value=":sparkles: An entire complete sign in user flow" />
|
<MESSAGE value=":sparkles: An entire complete sign in user flow" />
|
||||||
<MESSAGE value=":sparkles: User center page" />
|
<MESSAGE value=":sparkles: User center page" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: User center page" />
|
<MESSAGE value=":sparkles: Personalize" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Personalize" />
|
||||||
</component>
|
</component>
|
||||||
<component name="VgoProject">
|
<component name="VgoProject">
|
||||||
<settings-migrated>true</settings-migrated>
|
<settings-migrated>true</settings-migrated>
|
||||||
|
@ -7,12 +7,16 @@
|
|||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
|
"approve": "Approve",
|
||||||
|
"decline": "Decline",
|
||||||
"magicToken": "Magic Token",
|
"magicToken": "Magic Token",
|
||||||
"signinTitle": "Sign In",
|
"signinTitle": "Sign In",
|
||||||
"signinCaption": "Sign in to Solarpass to explore entire Solar Network. Explore posts, discover communities, talk with your best friends. All these things in the Solar Network!",
|
"signinCaption": "Sign in to Solarpass to explore entire Solar Network. Explore posts, discover communities, talk with your best friends. All these things in the Solar Network!",
|
||||||
"signinRequired": "You need to sign in before do that.",
|
"signinRequired": "You need to sign in before do that.",
|
||||||
"signupTitle": "Sign Up",
|
"signupTitle": "Sign Up",
|
||||||
"signupCaption": "Sign up to create an account on Solarpass, then you can explore the entire Solar Network! Enjoy the next-generation Internet Ecosystem!",
|
"signupCaption": "Sign up to create an account on Solarpass, then you can explore the entire Solar Network! Enjoy the next-generation Internet Ecosystem!",
|
||||||
|
"authorizeTitle": "Authorize",
|
||||||
|
"authorizeCaption": "One Solarpass, get entire network.",
|
||||||
"mfaTitle": "Multi Factor Authenticate",
|
"mfaTitle": "Multi Factor Authenticate",
|
||||||
"mfaCaption": "We need use one more way to verify it is you.",
|
"mfaCaption": "We need use one more way to verify it is you.",
|
||||||
"mfaFactorEmail": "OTP through your email"
|
"mfaFactorEmail": "OTP through your email"
|
||||||
|
@ -7,12 +7,16 @@
|
|||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"apply": "应用",
|
"apply": "应用",
|
||||||
"back": "返回",
|
"back": "返回",
|
||||||
|
"approve": "接受",
|
||||||
|
"decline": "拒绝",
|
||||||
"magicToken": "魔法令牌",
|
"magicToken": "魔法令牌",
|
||||||
"signinTitle": "登陆",
|
"signinTitle": "登陆",
|
||||||
"signinCaption": "登陆 Solarpass 以探索整个 Solar Network,浏览帖子、探索社区、和你的好朋友聊八卦,一切尽在 Solar Network!",
|
"signinCaption": "登陆 Solarpass 以探索整个 Solar Network,浏览帖子、探索社区、和你的好朋友聊八卦,一切尽在 Solar Network!",
|
||||||
"signinRequired": "你需要在那之前登陆",
|
"signinRequired": "你需要在那之前登陆",
|
||||||
"signupTitle": "注册",
|
"signupTitle": "注册",
|
||||||
"signupCaption": "注册以在 Solarpass 创建一个账号,之后你就可以探索整个 Solar Network,享受下一代互联网生态系统!",
|
"signupCaption": "注册以在 Solarpass 创建一个账号,之后你就可以探索整个 Solar Network,享受下一代互联网生态系统!",
|
||||||
|
"authorizeTitle": "授权",
|
||||||
|
"authorizeCaption": "一个 Solarpass,整个网络。",
|
||||||
"mfaTitle": "多因素验证",
|
"mfaTitle": "多因素验证",
|
||||||
"mfaCaption": "我们需要另一个方法来确认你是你。",
|
"mfaCaption": "我们需要另一个方法来确认你是你。",
|
||||||
"mfaFactorEmail": "电子邮寄一次性验证码"
|
"mfaFactorEmail": "电子邮寄一次性验证码"
|
||||||
|
@ -29,11 +29,13 @@ func MapUserInterface(A *fiber.App, authFunc func(c *fiber.Ctx, overrides ...str
|
|||||||
pages.Get("/sign-in", signinPage)
|
pages.Get("/sign-in", signinPage)
|
||||||
pages.Get("/mfa", mfaRequestPage)
|
pages.Get("/mfa", mfaRequestPage)
|
||||||
pages.Get("/mfa/apply", mfaApplyPage)
|
pages.Get("/mfa/apply", mfaApplyPage)
|
||||||
|
pages.Get("/authorize", authCheckWare, authorizePage)
|
||||||
|
|
||||||
pages.Post("/sign-up", signupAction)
|
pages.Post("/sign-up", signupAction)
|
||||||
pages.Post("/sign-in", signinAction)
|
pages.Post("/sign-in", signinAction)
|
||||||
pages.Post("/mfa", mfaRequestAction)
|
pages.Post("/mfa", mfaRequestAction)
|
||||||
pages.Post("/mfa/apply", mfaApplyAction)
|
pages.Post("/mfa/apply", mfaApplyAction)
|
||||||
|
pages.Post("/authorize", authCheckWare, authorizeAction)
|
||||||
|
|
||||||
pages.Get("/users/me", authCheckWare, selfUserinfoPage)
|
pages.Get("/users/me", authCheckWare, selfUserinfoPage)
|
||||||
pages.Get("/users/me/personalize", authCheckWare, personalizePage)
|
pages.Get("/users/me/personalize", authCheckWare, personalizePage)
|
||||||
|
154
pkg/server/ui/oauth.go
Normal file
154
pkg/server/ui/oauth.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||||
|
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||||
|
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/sujit-baniya/flash"
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authorizePage(c *fiber.Ctx) error {
|
||||||
|
localizer := c.Locals("localizer").(*i18n.Localizer)
|
||||||
|
user := c.Locals("principal").(models.Account)
|
||||||
|
|
||||||
|
id := c.Query("client_id")
|
||||||
|
redirect := c.Query("redirect_uri")
|
||||||
|
|
||||||
|
var message string
|
||||||
|
if len(id) <= 0 || len(redirect) <= 0 {
|
||||||
|
message = "invalid request, missing query parameters"
|
||||||
|
}
|
||||||
|
|
||||||
|
var client models.ThirdClient
|
||||||
|
if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
|
||||||
|
message = fmt.Sprintf("unable to find client: %v", err)
|
||||||
|
} else if !client.IsDraft && !lo.Contains(client.Callbacks, strings.Split(redirect, "?")[0]) {
|
||||||
|
message = "invalid callback url"
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticket models.AuthTicket
|
||||||
|
if err := database.C.Where(&models.AuthTicket{
|
||||||
|
AccountID: user.ID,
|
||||||
|
ClientID: &client.ID,
|
||||||
|
}).Where("last_grant_at IS NULL").First(&ticket).Error; err == nil {
|
||||||
|
if !(ticket.ExpiredAt != nil && ticket.ExpiredAt.Unix() < time.Now().Unix()) {
|
||||||
|
ticket, err = services.RegenSession(ticket)
|
||||||
|
if c.Query("response_type") == "code" {
|
||||||
|
return c.Redirect(fmt.Sprintf(
|
||||||
|
"%s?code=%s&state=%s",
|
||||||
|
redirect,
|
||||||
|
*ticket.GrantToken,
|
||||||
|
c.Query("state"),
|
||||||
|
))
|
||||||
|
} else if c.Query("response_type") == "token" {
|
||||||
|
if access, refresh, err := services.GetToken(ticket); err == nil {
|
||||||
|
return c.Redirect(fmt.Sprintf("%s?access_token=%s&refresh_token=%s&state=%s",
|
||||||
|
redirect,
|
||||||
|
access,
|
||||||
|
refresh, c.Query("state"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decline, _ := localizer.LocalizeMessage(&i18n.Message{ID: "decline"})
|
||||||
|
approve, _ := localizer.LocalizeMessage(&i18n.Message{ID: "approve"})
|
||||||
|
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "authorizeTitle"})
|
||||||
|
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "authorizeCaption"})
|
||||||
|
|
||||||
|
qs := "/authorize?" + string(c.Request().URI().QueryString())
|
||||||
|
|
||||||
|
return c.Render("views/authorize", fiber.Map{
|
||||||
|
"info": lo.Ternary[any](len(message) > 0, message, flash.Get(c)["message"]),
|
||||||
|
"client": client,
|
||||||
|
"scopes": strings.Split(c.Query("scope"), " "),
|
||||||
|
"action_url": template.URL(qs),
|
||||||
|
"i18n": fiber.Map{
|
||||||
|
"approve": approve,
|
||||||
|
"decline": decline,
|
||||||
|
"title": title,
|
||||||
|
"caption": caption,
|
||||||
|
},
|
||||||
|
}, "views/layouts/auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorizeAction(c *fiber.Ctx) error {
|
||||||
|
user := c.Locals("principal").(models.Account)
|
||||||
|
id := c.Query("client_id")
|
||||||
|
response := c.Query("response_type")
|
||||||
|
redirect := c.Query("redirect_uri")
|
||||||
|
scope := c.Query("scope")
|
||||||
|
|
||||||
|
redirectBackUri := "/authorize?" + string(c.Request().URI().QueryString())
|
||||||
|
|
||||||
|
if len(scope) <= 0 {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": "invalid request parameters",
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
var client models.ThirdClient
|
||||||
|
if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case "code":
|
||||||
|
// OAuth Authorization Mode
|
||||||
|
ticket, err := services.NewOauthTicket(
|
||||||
|
user,
|
||||||
|
client,
|
||||||
|
strings.Split(scope, " "),
|
||||||
|
[]string{"passport", client.Alias},
|
||||||
|
c.IP(),
|
||||||
|
c.Get(fiber.HeaderUserAgent),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
} else {
|
||||||
|
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||||
|
return c.Redirect(fmt.Sprintf(
|
||||||
|
"%s?code=%s&state=%s",
|
||||||
|
redirect,
|
||||||
|
*ticket.GrantToken,
|
||||||
|
c.Query("state"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case "token":
|
||||||
|
// OAuth Implicit Mode
|
||||||
|
ticket, err := services.NewOauthTicket(
|
||||||
|
user,
|
||||||
|
client,
|
||||||
|
strings.Split(scope, " "),
|
||||||
|
[]string{"passport", client.Alias},
|
||||||
|
c.IP(),
|
||||||
|
c.Get(fiber.HeaderUserAgent),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
} else if access, refresh, err := services.GetToken(ticket); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
} else {
|
||||||
|
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||||
|
return c.Redirect(fmt.Sprintf("%s?access_token=%s&refresh_token=%s&state=%s",
|
||||||
|
redirect,
|
||||||
|
access,
|
||||||
|
refresh, c.Query("state"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": "unsupported response type",
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
}
|
||||||
|
}
|
50
pkg/views/authorize.gohtml
Normal file
50
pkg/views/authorize.gohtml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<div class="left-part">
|
||||||
|
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/>
|
||||||
|
|
||||||
|
<h1 class="title">{{.i18n.title}} {{.client.Name}}</h1>
|
||||||
|
<p class="caption">{{.i18n.caption}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-part">
|
||||||
|
<div class="responsive-title-gap "></div>
|
||||||
|
|
||||||
|
<form class="action-form" action="{{.action_url}}" method="POST">
|
||||||
|
<div>
|
||||||
|
<div class="section-title">Description</div>
|
||||||
|
<div class="section-body">{{.client.Description}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="section-title">Requested scopes</div>
|
||||||
|
<ul class="section-scope">
|
||||||
|
{{range $_, $element := .scopes}}
|
||||||
|
<li>
|
||||||
|
<span class="section-mono">{{$element}}</span>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-form-buttons">
|
||||||
|
<md-text-button type="button" id="decline-button">{{.i18n.decline}}</md-text-button>
|
||||||
|
<md-filled-button type="submit">{{.i18n.approve}}</md-filled-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-mono {
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById("decline-button").addEventListener("click", () => {
|
||||||
|
history.back()
|
||||||
|
window.close()
|
||||||
|
})
|
||||||
|
</script>
|
@ -121,14 +121,6 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
|
||||||
margin: 1rem 0.2rem;
|
|
||||||
border-top: 1px solid var(--md-sys-color-on-surface);
|
|
||||||
border-left: none !important;
|
|
||||||
border-right: none !important;
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.wrapper-card {
|
.wrapper-card {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="divider"/>
|
<md-divider style="margin: 1rem 0"></md-divider>
|
||||||
|
|
||||||
<form class="action-form" action="/users/me/personalize" method="POST">
|
<form class="action-form" action="/users/me/personalize" method="POST">
|
||||||
<div class="columns-two">
|
<div class="columns-two">
|
||||||
|
Loading…
Reference in New Issue
Block a user