mirror of
https://github.com/h44z/wg-portal.git
synced 2026-05-28 08:56:17 +00:00
fix vue and oauth redirects under web base path (#683)
This commit is contained in:
@@ -6,8 +6,10 @@ import {authStore} from '@/stores/auth'
|
||||
import {securityStore} from '@/stores/security'
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
|
||||
const routerBase = `${WGPORTAL_BASE_PATH || ''}${import.meta.env.BASE_URL || '/'}`
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
history: createWebHashHistory(routerBase),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
|
||||
@@ -83,7 +83,9 @@ const externalLogin = function (provider) {
|
||||
console.log("Performing external login for provider", provider.Identifier);
|
||||
loggingIn.value = true;
|
||||
console.log(router.currentRoute.value);
|
||||
let currentUri = window.location.origin + "/#" + router.currentRoute.value.fullPath;
|
||||
const currentUrl = new URL(`${WGPORTAL_BASE_PATH || ''}${import.meta.env.BASE_URL || '/'}`, window.location.origin);
|
||||
currentUrl.hash = router.currentRoute.value.fullPath;
|
||||
let currentUri = currentUrl.toString();
|
||||
let redirectUrl = `${WGPORTAL_BACKEND_BASE_URL}${provider.ProviderUrl}`;
|
||||
redirectUrl += "?redirect=true";
|
||||
redirectUrl += "&return=" + encodeURIComponent(currentUri);
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
@@ -201,9 +202,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
provider := request.Path(r, "provider")
|
||||
|
||||
var returnUrl *url.URL
|
||||
var returnParams string
|
||||
redirectToReturn := func() {
|
||||
respond.Redirect(w, r, http.StatusFound, returnUrl.String()+"?"+returnParams)
|
||||
redirectToReturn := func(loginState string) {
|
||||
respond.Redirect(w, r, http.StatusFound, e.returnUrlWithLoginState(returnUrl, loginState))
|
||||
}
|
||||
|
||||
if returnTo != "" {
|
||||
@@ -212,21 +212,18 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
model.Error{Code: http.StatusBadRequest, Message: "invalid return URL"})
|
||||
return
|
||||
}
|
||||
if u, err := url.Parse(returnTo); err == nil {
|
||||
returnUrl = u
|
||||
u, err := url.Parse(returnTo)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "invalid return URL"})
|
||||
return
|
||||
}
|
||||
queryParams := returnUrl.Query()
|
||||
queryParams.Set("wgLoginState", "err") // by default, we set the state to error
|
||||
returnUrl.RawQuery = "" // remove potential query params
|
||||
returnParams = queryParams.Encode()
|
||||
returnUrl = u
|
||||
}
|
||||
|
||||
if currentSession.LoggedIn {
|
||||
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
||||
queryParams := returnUrl.Query()
|
||||
queryParams.Set("wgLoginState", "success")
|
||||
returnParams = queryParams.Encode()
|
||||
redirectToReturn()
|
||||
if autoRedirect && returnUrl != nil {
|
||||
redirectToReturn("success")
|
||||
} else {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "already logged in"})
|
||||
@@ -238,8 +235,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
if err != nil {
|
||||
slog.Debug("failed to create oauth auth code URL",
|
||||
"provider", provider, "error", err)
|
||||
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
||||
redirectToReturn()
|
||||
if autoRedirect && returnUrl != nil {
|
||||
redirectToReturn("err")
|
||||
} else {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
@@ -278,27 +275,19 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
var returnUrl *url.URL
|
||||
var returnParams string
|
||||
redirectToReturn := func() {
|
||||
respond.Redirect(w, r, http.StatusFound, returnUrl.String()+"?"+returnParams)
|
||||
redirectToReturn := func(loginState string) {
|
||||
respond.Redirect(w, r, http.StatusFound, e.returnUrlWithLoginState(returnUrl, loginState))
|
||||
}
|
||||
|
||||
if currentSession.OauthReturnTo != "" {
|
||||
if currentSession.OauthReturnTo != "" && e.isValidReturnUrl(currentSession.OauthReturnTo) {
|
||||
if u, err := url.Parse(currentSession.OauthReturnTo); err == nil {
|
||||
returnUrl = u
|
||||
}
|
||||
queryParams := returnUrl.Query()
|
||||
queryParams.Set("wgLoginState", "err") // by default, we set the state to error
|
||||
returnUrl.RawQuery = "" // remove potential query params
|
||||
returnParams = queryParams.Encode()
|
||||
}
|
||||
|
||||
if currentSession.LoggedIn {
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
queryParams := returnUrl.Query()
|
||||
queryParams.Set("wgLoginState", "success")
|
||||
returnParams = queryParams.Encode()
|
||||
redirectToReturn()
|
||||
if returnUrl != nil {
|
||||
redirectToReturn("success")
|
||||
} else {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Message: "already logged in"})
|
||||
}
|
||||
@@ -312,8 +301,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
if provider != currentSession.OauthProvider {
|
||||
slog.Debug("invalid oauth provider in callback",
|
||||
"expected", currentSession.OauthProvider, "got", provider, "state", oauthState)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
if returnUrl != nil {
|
||||
redirectToReturn("err")
|
||||
} else {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "invalid oauth provider"})
|
||||
@@ -323,8 +312,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
if oauthState != currentSession.OauthState {
|
||||
slog.Debug("invalid oauth state in callback",
|
||||
"expected", currentSession.OauthState, "got", oauthState, "provider", provider)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
if returnUrl != nil {
|
||||
redirectToReturn("err")
|
||||
} else {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "invalid oauth state"})
|
||||
@@ -339,8 +328,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
if err != nil {
|
||||
slog.Debug("failed to process oauth code",
|
||||
"provider", provider, "state", oauthState, "error", err)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
if returnUrl != nil {
|
||||
redirectToReturn("err")
|
||||
} else {
|
||||
respond.JSON(w, http.StatusUnauthorized,
|
||||
model.Error{Code: http.StatusUnauthorized, Message: err.Error()})
|
||||
@@ -350,11 +339,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
|
||||
e.setAuthenticatedUser(r, user, provider, idTokenHint)
|
||||
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
queryParams := returnUrl.Query()
|
||||
queryParams.Set("wgLoginState", "success")
|
||||
returnParams = queryParams.Encode()
|
||||
redirectToReturn()
|
||||
if returnUrl != nil {
|
||||
redirectToReturn("success")
|
||||
} else {
|
||||
respond.JSON(w, http.StatusOK, user)
|
||||
}
|
||||
@@ -444,11 +430,7 @@ func (e AuthEndpoint) handleLogoutPost() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
postLogoutRedirectUri := e.cfg.Web.ExternalUrl
|
||||
if e.cfg.Web.BasePath != "" {
|
||||
postLogoutRedirectUri += e.cfg.Web.BasePath
|
||||
}
|
||||
postLogoutRedirectUri += "/#/login"
|
||||
postLogoutRedirectUri := e.frontendUrl("/login")
|
||||
|
||||
var redirectUrl *string
|
||||
if currentSession.OauthProvider != "" {
|
||||
@@ -479,9 +461,60 @@ func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if e.cfg.Web.BasePath != "" {
|
||||
expectedPath := e.cfg.Web.BasePath + "/app"
|
||||
if returnUrlParsed.Path != expectedPath && !strings.HasPrefix(returnUrlParsed.Path, expectedPath+"/") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (e AuthEndpoint) frontendUrl(route string) string {
|
||||
frontendUrl := e.cfg.Web.ExternalUrl + e.cfg.Web.BasePath + "/app/"
|
||||
if route != "" {
|
||||
frontendUrl += "#" + route
|
||||
}
|
||||
return frontendUrl
|
||||
}
|
||||
|
||||
func (e AuthEndpoint) returnUrlWithLoginState(returnUrl *url.URL, loginState string) string {
|
||||
if returnUrl == nil {
|
||||
frontendURL, err := url.Parse(e.frontendUrl("/login"))
|
||||
if err != nil {
|
||||
return e.frontendUrl("/login")
|
||||
}
|
||||
returnUrl = frontendURL
|
||||
}
|
||||
|
||||
redirectUrl := *returnUrl
|
||||
|
||||
if redirectUrl.Fragment != "" {
|
||||
fragmentPath := redirectUrl.Fragment
|
||||
fragmentQuery := ""
|
||||
if queryStart := strings.Index(fragmentPath, "?"); queryStart >= 0 {
|
||||
fragmentQuery = fragmentPath[queryStart+1:]
|
||||
fragmentPath = fragmentPath[:queryStart]
|
||||
}
|
||||
|
||||
queryParams, err := url.ParseQuery(fragmentQuery)
|
||||
if err != nil {
|
||||
queryParams = url.Values{}
|
||||
}
|
||||
queryParams.Set("wgLoginState", loginState)
|
||||
redirectUrl.Fragment = fragmentPath + "?" + queryParams.Encode()
|
||||
|
||||
return redirectUrl.String()
|
||||
}
|
||||
|
||||
queryParams := redirectUrl.Query()
|
||||
queryParams.Set("wgLoginState", loginState)
|
||||
redirectUrl.RawQuery = queryParams.Encode()
|
||||
|
||||
return redirectUrl.String()
|
||||
}
|
||||
|
||||
// handleWebAuthnCredentialsGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID auth_handleWebAuthnCredentialsGet
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
)
|
||||
|
||||
type testSession struct {
|
||||
data SessionData
|
||||
}
|
||||
|
||||
func (s *testSession) SetData(_ context.Context, val SessionData) {
|
||||
s.data = val
|
||||
}
|
||||
|
||||
func (s *testSession) GetData(_ context.Context) SessionData {
|
||||
return s.data
|
||||
}
|
||||
|
||||
func (s *testSession) DestroyData(_ context.Context) {
|
||||
s.data = SessionData{}
|
||||
}
|
||||
|
||||
func newBasePathAuthEndpoint(session Session) AuthEndpoint {
|
||||
return AuthEndpoint{
|
||||
cfg: &config.Config{
|
||||
Web: config.WebConfig{
|
||||
ExternalUrl: "https://wg.example.com",
|
||||
BasePath: "/subpath",
|
||||
},
|
||||
},
|
||||
session: session,
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthEndpointIsValidReturnUrlRequiresBasePathApp(t *testing.T) {
|
||||
ep := newBasePathAuthEndpoint(&testSession{})
|
||||
|
||||
valid := []string{
|
||||
"https://wg.example.com/subpath/app/#/login",
|
||||
"https://wg.example.com/subpath/app/#/login?all=true",
|
||||
"https://wg.example.com/subpath/app/?beforeHash=true#/login",
|
||||
}
|
||||
for _, returnURL := range valid {
|
||||
if !ep.isValidReturnUrl(returnURL) {
|
||||
t.Fatalf("expected return URL to be valid: %s", returnURL)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"https://wg.example.com/#/login",
|
||||
"https://wg.example.com/subpath/#/login",
|
||||
"https://other.example.com/subpath/app/#/login",
|
||||
}
|
||||
for _, returnURL := range invalid {
|
||||
if ep.isValidReturnUrl(returnURL) {
|
||||
t.Fatalf("expected return URL to be invalid: %s", returnURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthEndpointOauthCallbackRedirectsToBasePathHashRoute(t *testing.T) {
|
||||
session := &testSession{data: SessionData{
|
||||
LoggedIn: true,
|
||||
OauthReturnTo: "https://wg.example.com/subpath/app/#/login",
|
||||
}}
|
||||
ep := newBasePathAuthEndpoint(session)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v0/auth/login/google/callback", nil)
|
||||
req.SetPathValue("provider", "google")
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
ep.handleOauthCallbackGet().ServeHTTP(res, req)
|
||||
|
||||
if res.Code != http.StatusFound {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusFound, res.Code)
|
||||
}
|
||||
if got, want := res.Header().Get("Location"), "https://wg.example.com/subpath/app/#/login?wgLoginState=success"; got != want {
|
||||
t.Fatalf("expected redirect %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthEndpointReturnUrlWithLoginStatePreservesHashQuery(t *testing.T) {
|
||||
session := &testSession{data: SessionData{
|
||||
LoggedIn: true,
|
||||
OauthReturnTo: "https://wg.example.com/subpath/app/#/login?all=true",
|
||||
}}
|
||||
ep := newBasePathAuthEndpoint(session)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v0/auth/login/google/callback", nil)
|
||||
req.SetPathValue("provider", "google")
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
ep.handleOauthCallbackGet().ServeHTTP(res, req)
|
||||
|
||||
if res.Code != http.StatusFound {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusFound, res.Code)
|
||||
}
|
||||
if got, want := res.Header().Get("Location"), "https://wg.example.com/subpath/app/#/login?all=true&wgLoginState=success"; got != want {
|
||||
t.Fatalf("expected redirect %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthEndpointFrontendUrlUsesBasePathAppMount(t *testing.T) {
|
||||
ep := newBasePathAuthEndpoint(&testSession{})
|
||||
|
||||
if got, want := ep.frontendUrl("/login"), "https://wg.example.com/subpath/app/#/login"; got != want {
|
||||
t.Fatalf("expected frontend URL %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user