diff --git a/consts/redis_key.go b/consts/redis_key.go index 4767eff..1b82715 100644 --- a/consts/redis_key.go +++ b/consts/redis_key.go @@ -1,7 +1,8 @@ package consts const ( - REDIS_KEY_LOGIN_SESSION = "misso:login:%s" // Username, token as value - REDIS_KEY_CONSENT_CSRF = "misso:consent:%s" // Random string, consent challenge as value - REDIS_KEY_SHARE_CONTEXT = "misso:share:%s" // Subject, context as value + REDIS_KEY_LOGIN_SESSION = "misso:login:%s" // Username, token as value + REDIS_KEY_CONSENT_CSRF = "misso:consent:%s" // Random string, consent challenge as value + REDIS_KEY_SHARE_ACCESS_TOKEN = "misso:share:at:%s" // Subject, access token as value + REDIS_KEY_SHARE_USER_INFO = "misso:share:ui:%s" // Subject, user info as value ) diff --git a/consts/time.go b/consts/time.go index c479be3..f6233e9 100644 --- a/consts/time.go +++ b/consts/time.go @@ -3,9 +3,9 @@ package consts import "time" const ( - TIME_LOGIN_REQUEST_VALID = 10 * time.Minute - TIME_LOGIN_SESSION_VALID = 7 * 24 * time.Hour + TIME_REQUEST_VALID = 1 * time.Hour - TIME_CONSENT_REQUEST_VALID = 1 * time.Hour - TIME_CONSENT_SESSION_VALID = 30 * 24 * time.Hour + TIME_LOGIN_REMEMBER = 10 * time.Minute + + TIME_USERINFO_CACHE = 10 * time.Minute ) diff --git a/handlers/consent/consent_check.go b/handlers/consent/consent_check.go index ec5e116..7503f44 100644 --- a/handlers/consent/consent_check.go +++ b/handlers/consent/consent_check.go @@ -2,13 +2,11 @@ package consent import ( "context" - "encoding/json" "fmt" "github.com/gin-gonic/gin" client "github.com/ory/hydra-client-go/v2" "misso/consts" "misso/global" - "misso/types" "misso/utils" "net/http" ) @@ -59,7 +57,7 @@ func ConsentCheck(ctx *gin.Context) { global.Logger.Debugf("Generating CSRF token...") csrf := utils.RandString(32) sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, csrf) - err := global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_CONSENT_REQUEST_VALID).Err() + err := global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_REQUEST_VALID).Err() if err != nil { global.Logger.Errorf("Failed to save csrf into redis with error: %v", err) ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ @@ -70,23 +68,12 @@ func ConsentCheck(ctx *gin.Context) { // Retrieve context global.Logger.Debugf("Retrieving context...") - var userinfoCtx types.SessionContext - sessKey = fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, *consentReq.Subject) - userinfoCtxBytes, err := global.Redis.Get(context.Background(), sessKey).Bytes() - if err != nil { - global.Logger.Errorf("Failed to retrieve context with error: %v", err) - ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": "Failed to retrieve context", - }) - return - } - global.Logger.Debugf("Decoding context...") - err = json.Unmarshal(userinfoCtxBytes, &userinfoCtx) + userinfoCtx, err := utils.GetUserinfo(*consentReq.Subject) if err != nil { - global.Logger.Errorf("Failed to parse context with error: %v", err) + global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err) ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": "Failed to parse context", + "error": "Failed to get userinfo", }) return } @@ -94,7 +81,7 @@ func ConsentCheck(ctx *gin.Context) { // Show the consent UI global.Logger.Debugf("Rendering consent UI...") templateFields := gin.H{ - "user": userinfoCtx.User, + "user": *userinfoCtx, "challenge": oauth2challenge, "csrf": csrf, } diff --git a/handlers/consent/consent_confirm.go b/handlers/consent/consent_confirm.go index 03e4751..dbced75 100644 --- a/handlers/consent/consent_confirm.go +++ b/handlers/consent/consent_confirm.go @@ -2,15 +2,12 @@ package consent import ( "context" - "encoding/json" "fmt" "github.com/gin-gonic/gin" client "github.com/ory/hydra-client-go/v2" "misso/consts" "misso/global" - "misso/types" "net/http" - "time" ) type ConsentConfirmRequest struct { @@ -80,31 +77,9 @@ func ConsentConfirm(ctx *gin.Context) { global.Logger.Debugf("User should now be redirecting to target URI.") } else if req.Action == "accept" { global.Logger.Debugf("User accepted the request, reporting back to hydra...") - // Retrieve context - global.Logger.Debugf("Retrieving context...") - var userinfoCtx types.SessionContext - sessKey = fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, *consentReq.Subject) - userinfoCtxBytes, err := global.Redis.Get(context.Background(), sessKey).Bytes() - if err != nil { - global.Logger.Errorf("Failed to retrieve context with error: %v", err) - ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": "Failed to retrieve context", - }) - return - } - - global.Logger.Debugf("Decoding context...") - err = json.Unmarshal(userinfoCtxBytes, &userinfoCtx) - if err != nil { - global.Logger.Errorf("Failed to parse context with error: %v", err) - ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": "Failed to parse context", - }) - return - } global.Logger.Debugf("Initializing ID Token...") - rememberFor := int64(consts.TIME_CONSENT_SESSION_VALID / time.Second) + rememberFor := int64(0) // Remember forever acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2ConsentRequest(context.Background()).ConsentChallenge(oauth2challenge).AcceptOAuth2ConsentRequest(client.AcceptOAuth2ConsentRequest{ GrantScope: consentReq.RequestedScope, // TODO: Specify scopes GrantAccessTokenAudience: consentReq.RequestedAccessTokenAudience, diff --git a/handlers/login/login.go b/handlers/login/login.go index e902448..a5bac0f 100644 --- a/handlers/login/login.go +++ b/handlers/login/login.go @@ -65,9 +65,11 @@ func Login(ctx *gin.Context) { return } + global.Logger.Debugf("Grabbed auth session: %s", authSess.Token) + // Save login challenge state into redis (misskey cannot keep state info) sessKey := fmt.Sprintf(consts.REDIS_KEY_LOGIN_SESSION, authSess.Token) - err = global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_LOGIN_REQUEST_VALID).Err() + err = global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_REQUEST_VALID).Err() if err != nil { global.Logger.Errorf("Failed to save session into redis with error: %v", err) ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ diff --git a/handlers/login/misskey_auth_callback.go b/handlers/login/misskey_auth_callback.go index 8cf54c7..bdfc35a 100644 --- a/handlers/login/misskey_auth_callback.go +++ b/handlers/login/misskey_auth_callback.go @@ -2,7 +2,6 @@ package login import ( "context" - "encoding/json" "fmt" "github.com/gin-gonic/gin" client "github.com/ory/hydra-client-go/v2" @@ -10,7 +9,7 @@ import ( "misso/consts" "misso/global" "misso/misskey" - "misso/types" + "misso/utils" "net/http" "time" ) @@ -65,31 +64,29 @@ func MisskeyAuthCallback(ctx *gin.Context) { userid := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance) - // Save context into redis - userinfoCtxBytes, err := json.Marshal(&types.SessionContext{ - MisskeyToken: usermeta.AccessToken, - User: usermeta.User, - }) + sessAccessTokenKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, userid) + err = global.Redis.Set(context.Background(), sessAccessTokenKey, usermeta.AccessToken, 0).Err() if err != nil { - global.Logger.Errorf("Failed to parse accept context with error: %v", err) - ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": "Failed to parse accept context", - }) - return - } - sessKey = fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, userid) - err = global.Redis.Set(context.Background(), sessKey, userinfoCtxBytes, consts.TIME_LOGIN_SESSION_VALID).Err() - if err != nil { - global.Logger.Errorf("Failed to save session into redis with error: %v", err) + global.Logger.Errorf("Failed to save session access token into redis with error: %v", err) ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ "error": "Failed to save context", }) return } + // Save context into redis + err = utils.SaveUserinfo(userid, &usermeta.User) + if err != nil { + global.Logger.Errorf("Failed to save session user info into redis with error: %v", err) + ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ + "error": "Failed to save userinfo", + }) + return + } + global.Logger.Debugf("User accepted the request, reporting back to hydra...") remember := true - rememberFor := int64(consts.TIME_LOGIN_SESSION_VALID / time.Second) + rememberFor := int64(consts.TIME_LOGIN_REMEMBER / time.Second) acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LoginRequest(context.Background()).LoginChallenge(oauth2challenge).AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{ Subject: userid, Remember: &remember, diff --git a/handlers/logout/logout.go b/handlers/logout/logout.go new file mode 100644 index 0000000..4988ec9 --- /dev/null +++ b/handlers/logout/logout.go @@ -0,0 +1,30 @@ +package logout + +import ( + "context" + "github.com/gin-gonic/gin" + "misso/global" + "net/http" +) + +func Logout(ctx *gin.Context) { + oauth2challenge := ctx.Query("logout_challenge") // OAuth2 login + if oauth2challenge == "" { + ctx.HTML(http.StatusBadRequest, "error.tmpl", gin.H{ + "error": "Necessary challenge not provided", + }) + return + } + + acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LogoutRequest(context.Background()).LogoutChallenge(oauth2challenge).Execute() + if err != nil { + global.Logger.Errorf("Failed to accept logout request with error: %v", err) + ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ + "error": "Failed to accept logout request", + }) + return + } + + ctx.Redirect(http.StatusTemporaryRedirect, acceptReq.RedirectTo) + +} diff --git a/handlers/user/info.go b/handlers/user/info.go index 92db816..a318dc4 100644 --- a/handlers/user/info.go +++ b/handlers/user/info.go @@ -2,12 +2,10 @@ package user import ( "context" - "encoding/json" - "fmt" "github.com/gin-gonic/gin" - "misso/consts" "misso/global" "misso/types" + "misso/utils" "net/http" "strings" ) @@ -46,32 +44,18 @@ func UserInfo(ctx *gin.Context) { } // Return user info - - // Retrieve context global.Logger.Debugf("Retrieving context...") - var userinfoCtx types.SessionContext - sessKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, *tokenInfo.Sub) - userinfoCtxBytes, err := global.Redis.Get(context.Background(), sessKey).Bytes() + userinfoCtx, err := utils.GetUserinfo(*tokenInfo.Sub) if err != nil { - global.Logger.Errorf("Failed to retrieve context with error: %v", err) + global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err) ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": "Failed to retrieve context", - }) - return - } - - global.Logger.Debugf("Decoding context...") - err = json.Unmarshal(userinfoCtxBytes, &userinfoCtx) - if err != nil { - global.Logger.Errorf("Failed to parse context with error: %v", err) - ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": "Failed to parse context", + "error": "Failed to get userinfo", }) return } ctx.JSON(http.StatusOK, UserinfoResponse{ - MisskeyUser: userinfoCtx.User, + MisskeyUser: *userinfoCtx, EMail: *tokenInfo.Sub, }) diff --git a/misskey/base.go b/misskey/base.go index aaa0fbc..1651a51 100644 --- a/misskey/base.go +++ b/misskey/base.go @@ -18,7 +18,7 @@ type Error_Response struct { } `json:"error"` } -func PostAPIRequest[T AuthSessionGenerate_Response | AuthSessionUserkey_Response]( +func PostAPIRequest[T I_Response | AuthSessionGenerate_Response | AuthSessionUserkey_Response]( apiEndpointPath string, reqBody any, ) (*T, error) { // Prepare request diff --git a/misskey/get_userinfo.go b/misskey/get_userinfo.go new file mode 100644 index 0000000..07120dd --- /dev/null +++ b/misskey/get_userinfo.go @@ -0,0 +1,15 @@ +package misskey + +import "misso/types" + +type I_Request struct { + I string `json:"i"` +} + +type I_Response = types.MisskeyUser + +func GetUserinfo(accessToken string) (*I_Response, error) { + return PostAPIRequest[I_Response]("/api/i", &I_Request{ + I: accessToken, + }) +} diff --git a/routers/logout.go b/routers/logout.go new file mode 100644 index 0000000..a9cb421 --- /dev/null +++ b/routers/logout.go @@ -0,0 +1,10 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "misso/handlers/logout" +) + +func Logout(rg *gin.RouterGroup) { + rg.GET("/logout", logout.Logout) +} diff --git a/routers/router.go b/routers/router.go index 1a41dea..521e308 100644 --- a/routers/router.go +++ b/routers/router.go @@ -18,6 +18,9 @@ func R(e *gin.Engine) { // Consent Consent(rootGroup) + // Logout + Logout(rootGroup) + // Userinfo User(rootGroup) } diff --git a/templates/head.tmpl b/templates/head.tmpl index a6567e7..ae72b1d 100644 --- a/templates/head.tmpl +++ b/templates/head.tmpl @@ -10,13 +10,14 @@ #main { display: flex; flex-direction: column; - width: fit-content; text-align: center; align-items: center; color: white; - padding: 32px 40px; + padding: 40px; background-color: #21252b; border-radius: 12px; + margin: 0 40px; + width: 100%; max-width: 480px; word-break: break-word; } diff --git a/types/MisskeyUser.go b/types/MisskeyUser.go index 1d3c80a..25a7ca2 100644 --- a/types/MisskeyUser.go +++ b/types/MisskeyUser.go @@ -182,3 +182,5 @@ type MisskeyUser struct { EmailNotificationTypes []string `json:"emailNotificationTypes"` ShowTimelineReplies bool `json:"showTimelineReplies"` } + +// TODO: Find a better way to split necessary fields and additional fields diff --git a/types/SessionContext.go b/types/SessionContext.go deleted file mode 100644 index c71cdbc..0000000 --- a/types/SessionContext.go +++ /dev/null @@ -1,6 +0,0 @@ -package types - -type SessionContext struct { - MisskeyToken string `json:"token"` - User MisskeyUser `json:"user"` -} diff --git a/utils/get_userinfo.go b/utils/get_userinfo.go new file mode 100644 index 0000000..5d269d1 --- /dev/null +++ b/utils/get_userinfo.go @@ -0,0 +1,60 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "misso/consts" + "misso/global" + "misso/misskey" + "misso/types" +) + +func GetUserinfo(subject string) (*types.MisskeyUser, error) { + // Check cache key + global.Logger.Debugf("Checking userinfo cache...") + userinfoCacheKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_USER_INFO, subject) + exist, err := global.Redis.Exists(context.Background(), userinfoCacheKey).Result() + if err != nil { + global.Logger.Errorf("Failed to check userinfo exist status with error: %v", err) + } else if exist > 0 { + // Exist, get from cache + userinfoCacheBytes, err := global.Redis.Get(context.Background(), userinfoCacheKey).Bytes() + if err != nil { + global.Logger.Errorf("Userinfo cache exists, but failed to retrieve with error: %v", err) + } else { + // Parse into user + var userinfo types.MisskeyUser + err = json.Unmarshal(userinfoCacheBytes, &userinfo) + if err != nil { + global.Logger.Errorf("Failed to parse userinfo cache into json with error: %v", err) + } else { + // Works! + global.Logger.Debugf("Get cached userinfo successfully.") + return &userinfo, nil + } + } + } + + // Fallback to get info directly, we need user's access token. + global.Logger.Debugf("No cached userinfo found (or valid), trying to get latest response.") + accessTokenCacheKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, subject) + accessToken, err := global.Redis.Get(context.Background(), accessTokenCacheKey).Result() + if err != nil { + global.Logger.Errorf("Failed to get user access token with error: %v", err) + return nil, err + } + + // Get userinfo with access token + userinfo, err := misskey.GetUserinfo(accessToken) + if err != nil { + global.Logger.Errorf("Failed to get user info with saved access token with error: %v", err) + return nil, err + } + + // Save userinfo into redis + _ = SaveUserinfo(subject, userinfo) // Ignore errors + + return userinfo, nil + +} diff --git a/utils/save_userinfo.go b/utils/save_userinfo.go new file mode 100644 index 0000000..d77819c --- /dev/null +++ b/utils/save_userinfo.go @@ -0,0 +1,26 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "misso/consts" + "misso/global" + "misso/types" +) + +func SaveUserinfo(subject string, userinfo *types.MisskeyUser) error { + userinfoBytes, err := json.Marshal(userinfo) + if err != nil { + global.Logger.Errorf("Failed to parse accept context with error: %v", err) + return err + } + sessUserInfoKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_USER_INFO, subject) + err = global.Redis.Set(context.Background(), sessUserInfoKey, userinfoBytes, consts.TIME_USERINFO_CACHE).Err() + if err != nil { + global.Logger.Errorf("Failed to save session user info into redis with error: %v", err) + return err + } + + return nil +}