Compare commits

...

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
d2b36dc590 Fix poor social login UX: immediate redirect when provider_hint is set
- Add GetGothAuthUrl to idp/goth.go to build OAuth URL via goth's BeginAuth
- Add getProviderRedirectUrl to routers/static_filter.go for server-side 302
  redirect when provider_hint is in the /login/oauth/authorize URL
- Add redirectWithProviderHint to LoginPage.js as frontend fallback to redirect
  immediately after API response, without rendering the full login page"

Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-02-23 01:10:26 +00:00
copilot-swe-agent[bot]
24bab4454e Initial plan 2026-02-23 00:46:44 +00:00
3 changed files with 136 additions and 0 deletions

View File

@@ -423,6 +423,27 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
return &idp, nil
}
// GetGothAuthUrl builds the OAuth authorization URL for a goth-based provider using BeginAuth.
// It returns an empty string if the provider type is not supported by goth.
func GetGothAuthUrl(idpInfo *ProviderInfo, state string, redirectUrl string) (string, error) {
gothIdp, err := NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.ClientId2, idpInfo.ClientSecret2, redirectUrl, idpInfo.HostUrl)
if err != nil {
return "", err
}
session, err := gothIdp.Provider.BeginAuth(state)
if err != nil {
return "", err
}
authUrl, err := session.GetAuthURL()
if err != nil {
return "", err
}
return authUrl, nil
}
// SetHttpClient
// Goth's idp all implement the Client method, but since the goth.Provider interface does not provide to modify idp's client method, reflection is required
func (idp *GothIdProvider) SetHttpClient(client *http.Client) {

View File

@@ -16,9 +16,11 @@ package routers
import (
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -26,6 +28,7 @@ import (
"github.com/beego/beego/v2/server/web/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@@ -104,6 +107,86 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
return res, nil
}
// getProviderRedirectUrl builds a server-side redirect URL when provider_hint is present.
// It looks up the application by client_id, finds the matching provider, and returns
// the OAuth authorization URL for that provider, enabling an immediate redirect without
// rendering the Casdoor login page.
func getProviderRedirectUrl(ctx *context.Context) (string, error) {
providerHint := ctx.Input.Query("provider_hint")
if providerHint == "" {
return "", nil
}
clientId := ctx.Input.Query("client_id")
if clientId == "" {
return "", nil
}
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
return "", err
}
if application == nil {
return "", nil
}
// Find the first visible provider matching the hint
var matchedProvider *object.Provider
for _, providerItem := range application.Providers {
if providerItem.Provider != nil &&
providerItem.Provider.Name == providerHint &&
providerItem.IsProviderVisible() {
matchedProvider = providerItem.Provider
break
}
}
if matchedProvider == nil {
return "", nil
}
// Build the state parameter, matching the frontend's getStateFromQueryParams logic:
// state = btoa("?" + rawQuery + "&application=" + encodeURIComponent(appName) +
// "&provider=" + encodeURIComponent(providerName) + "&method=signup")
rawQuery := ctx.Request.URL.RawQuery
applicationName := application.Name
if application.IsShared {
applicationName = application.Name + "-org-" + application.Organization
}
stateStr := "?" + rawQuery +
"&application=" + url.QueryEscape(applicationName) +
"&provider=" + url.QueryEscape(matchedProvider.Name) +
"&method=signup"
state := base64.StdEncoding.EncodeToString([]byte(stateStr))
// Determine the redirect origin (matching frontend logic)
redirectOrigin := application.ForcedRedirectOrigin
if redirectOrigin == "" {
scheme := "https"
if ctx.Request.TLS == nil &&
ctx.Request.Header.Get("X-Forwarded-Proto") != "https" &&
ctx.Request.Header.Get("X-Forwarded-Ssl") != "on" {
scheme = "http"
}
redirectOrigin = scheme + "://" + ctx.Request.Host
}
redirectUrl := redirectOrigin + "/callback"
// Build OAuth URL using goth's BeginAuth for goth-based providers.
// Non-goth providers fall through to the frontend (return empty string).
idpInfo, err := object.FromProviderToIdpInfo(ctx, matchedProvider)
if err != nil {
return "", err
}
authUrl, err := idp.GetGothAuthUrl(idpInfo, state, redirectUrl)
if err != nil {
// Provider is not goth-based or initialization failed; let the frontend handle it
return "", nil
}
return authUrl, nil
}
func StaticFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
@@ -128,6 +211,14 @@ func StaticFilter(ctx *context.Context) {
return
}
if redirectUrl == "" {
redirectUrl, err = getProviderRedirectUrl(ctx)
if err != nil {
responseError(ctx, err.Error())
return
}
}
if redirectUrl != "" {
http.Redirect(ctx.ResponseWriter, ctx.Request, redirectUrl, http.StatusFound)
return

View File

@@ -175,6 +175,21 @@ class LoginPage extends React.Component {
});
}
redirectWithProviderHint(application) {
const providerHint = new URLSearchParams(window.location.search).get("provider_hint");
if (!providerHint || !application?.providers) {
return false;
}
const providerItem = application.providers.find(
item => item.provider?.name === providerHint && Setting.isProviderVisible(item)
);
if (providerItem) {
goToLink(Provider.getAuthUrl(application, providerItem.provider, "signup"));
return true;
}
return false;
}
getApplicationLogin() {
let loginParams;
if (this.state.type === "cas") {
@@ -188,6 +203,9 @@ class LoginPage extends React.Component {
.then((res) => {
if (res.status === "ok") {
const application = res.data;
if (this.redirectWithProviderHint(application)) {
return;
}
this.onUpdateApplication(application);
} else {
if (this.state.type === "device") {
@@ -218,6 +236,9 @@ class LoginPage extends React.Component {
});
return ;
}
if (this.redirectWithProviderHint(res.data)) {
return;
}
this.onUpdateApplication(res.data);
});
} else {
@@ -225,6 +246,9 @@ class LoginPage extends React.Component {
.then((res) => {
if (res.status === "ok") {
const application = res.data;
if (this.redirectWithProviderHint(application)) {
return;
}
this.onUpdateApplication(application);
this.setState({
applicationName: res.data.name,