OAuth

This commit is contained in:
LittleSheep 2024-04-21 17:18:00 +08:00
parent 8e315642a4
commit c25a1f5c82
8 changed files with 221 additions and 16 deletions

13
.idea/workspace.xml generated
View File

@ -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 &amp; Sign in" /> <MESSAGE value=":sparkles: Sign up &amp; 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>

View File

@ -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"

View File

@ -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": "电子邮寄一次性验证码"

View File

@ -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
View 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)
}
}

View 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>

View File

@ -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;

View File

@ -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">