mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-07 08:26:00 +00:00
fix(auth): preserve return target across oauth login
This commit is contained in:
parent
45e96be179
commit
ac32fce08f
13 changed files with 309 additions and 14 deletions
|
|
@ -5,20 +5,30 @@ import com.iflytek.skillhub.dto.ApiResponse;
|
|||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.dto.AuthMeResponse;
|
||||
import com.iflytek.skillhub.dto.AuthProviderResponse;
|
||||
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import com.iflytek.skillhub.exception.UnauthorizedException;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
public class AuthController extends BaseApiController {
|
||||
|
||||
public AuthController(ApiResponseFactory responseFactory) {
|
||||
private final OAuth2ClientProperties oAuth2ClientProperties;
|
||||
|
||||
public AuthController(ApiResponseFactory responseFactory,
|
||||
OAuth2ClientProperties oAuth2ClientProperties) {
|
||||
super(responseFactory);
|
||||
this.oAuth2ClientProperties = oAuth2ClientProperties;
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
|
|
@ -31,8 +41,27 @@ public class AuthController extends BaseApiController {
|
|||
}
|
||||
|
||||
@GetMapping("/providers")
|
||||
public ApiResponse<List<AuthProviderResponse>> providers() {
|
||||
var github = new AuthProviderResponse("github", "GitHub", "/oauth2/authorization/github");
|
||||
return ok("response.success.read", List.of(github));
|
||||
public ApiResponse<List<AuthProviderResponse>> providers(
|
||||
@RequestParam(name = "returnTo", required = false) String returnTo) {
|
||||
String sanitizedReturnTo = com.iflytek.skillhub.auth.oauth.OAuthLoginRedirectSupport.sanitizeReturnTo(returnTo);
|
||||
List<AuthProviderResponse> providers = new ArrayList<>(oAuth2ClientProperties.getRegistration().entrySet().stream()
|
||||
.sorted(Comparator.comparing(entry -> entry.getKey()))
|
||||
.map(entry -> new AuthProviderResponse(
|
||||
entry.getKey(),
|
||||
entry.getValue().getClientName() != null && !entry.getValue().getClientName().isBlank()
|
||||
? entry.getValue().getClientName()
|
||||
: entry.getKey(),
|
||||
buildAuthorizationUrl(entry.getKey(), sanitizedReturnTo)
|
||||
))
|
||||
.toList());
|
||||
return ok("response.success.read", providers);
|
||||
}
|
||||
|
||||
private String buildAuthorizationUrl(String registrationId, String returnTo) {
|
||||
String baseUrl = "/oauth2/authorization/" + registrationId;
|
||||
if (returnTo == null) {
|
||||
return baseUrl;
|
||||
}
|
||||
return baseUrl + "?returnTo=" + URLEncoder.encode(returnTo, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ import org.springframework.boot.test.mock.mockito.MockBean;
|
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
|
@ -25,6 +27,20 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = {
|
||||
"spring.security.oauth2.client.registration.github.client-name=GitHub",
|
||||
"spring.security.oauth2.client.registration.gitee.client-id=placeholder",
|
||||
"spring.security.oauth2.client.registration.gitee.client-secret=placeholder",
|
||||
"spring.security.oauth2.client.registration.gitee.provider=gitee",
|
||||
"spring.security.oauth2.client.registration.gitee.authorization-grant-type=authorization_code",
|
||||
"spring.security.oauth2.client.registration.gitee.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}",
|
||||
"spring.security.oauth2.client.registration.gitee.scope=user_info",
|
||||
"spring.security.oauth2.client.registration.gitee.client-name=Gitee",
|
||||
"spring.security.oauth2.client.provider.gitee.authorization-uri=https://gitee.com/oauth/authorize",
|
||||
"spring.security.oauth2.client.provider.gitee.token-uri=https://gitee.com/oauth/token",
|
||||
"spring.security.oauth2.client.provider.gitee.user-info-uri=https://gitee.com/api/v5/user",
|
||||
"spring.security.oauth2.client.provider.gitee.user-name-attribute=id"
|
||||
})
|
||||
class AuthControllerTest {
|
||||
|
||||
@Autowired
|
||||
|
|
@ -79,9 +95,23 @@ class AuthControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.msg").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data[0].id").value("github"))
|
||||
.andExpect(jsonPath("$.data[0].authorizationUrl").value("/oauth2/authorization/github"))
|
||||
.andExpect(jsonPath("$.data.length()").value(2))
|
||||
.andExpect(jsonPath("$.data[*].id", hasItems("github", "gitee")))
|
||||
.andExpect(jsonPath("$.data[*].authorizationUrl", hasItems(
|
||||
"/oauth2/authorization/github",
|
||||
"/oauth2/authorization/gitee"
|
||||
)))
|
||||
.andExpect(jsonPath("$.timestamp").isNotEmpty())
|
||||
.andExpect(jsonPath("$.requestId").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void providersShouldAppendReturnToWhenRequested() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/auth/providers").param("returnTo", "/dashboard/publish"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data[*].authorizationUrl", hasItems(
|
||||
"/oauth2/authorization/github?returnTo=%2Fdashboard%2Fpublish",
|
||||
"/oauth2/authorization/gitee?returnTo=%2Fdashboard%2Fpublish"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.iflytek.skillhub.auth.config;
|
|||
import com.iflytek.skillhub.auth.oauth.CustomOAuth2UserService;
|
||||
import com.iflytek.skillhub.auth.oauth.OAuth2LoginFailureHandler;
|
||||
import com.iflytek.skillhub.auth.oauth.OAuth2LoginSuccessHandler;
|
||||
import com.iflytek.skillhub.auth.oauth.SkillHubOAuth2AuthorizationRequestResolver;
|
||||
import com.iflytek.skillhub.auth.mock.MockAuthFilter;
|
||||
import com.iflytek.skillhub.auth.token.ApiTokenAuthenticationFilter;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
|
@ -30,6 +31,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|||
public class SecurityConfig {
|
||||
|
||||
private final CustomOAuth2UserService customOAuth2UserService;
|
||||
private final SkillHubOAuth2AuthorizationRequestResolver authorizationRequestResolver;
|
||||
private final OAuth2LoginSuccessHandler successHandler;
|
||||
private final OAuth2LoginFailureHandler failureHandler;
|
||||
private final ApiTokenAuthenticationFilter apiTokenAuthenticationFilter;
|
||||
|
|
@ -38,6 +40,7 @@ public class SecurityConfig {
|
|||
private final ObjectProvider<MockAuthFilter> mockAuthFilterProvider;
|
||||
|
||||
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService,
|
||||
SkillHubOAuth2AuthorizationRequestResolver authorizationRequestResolver,
|
||||
OAuth2LoginSuccessHandler successHandler,
|
||||
OAuth2LoginFailureHandler failureHandler,
|
||||
ApiTokenAuthenticationFilter apiTokenAuthenticationFilter,
|
||||
|
|
@ -45,6 +48,7 @@ public class SecurityConfig {
|
|||
AccessDeniedHandler apiAccessDeniedHandler,
|
||||
ObjectProvider<MockAuthFilter> mockAuthFilterProvider) {
|
||||
this.customOAuth2UserService = customOAuth2UserService;
|
||||
this.authorizationRequestResolver = authorizationRequestResolver;
|
||||
this.successHandler = successHandler;
|
||||
this.failureHandler = failureHandler;
|
||||
this.apiTokenAuthenticationFilter = apiTokenAuthenticationFilter;
|
||||
|
|
@ -101,6 +105,7 @@ public class SecurityConfig {
|
|||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2Login(oauth2 -> oauth2
|
||||
.authorizationEndpoint(endpoint -> endpoint.authorizationRequestResolver(authorizationRequestResolver))
|
||||
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
|
||||
.successHandler(successHandler)
|
||||
.failureHandler(failureHandler)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ package com.iflytek.skillhub.auth.oauth;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Component
|
||||
public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
|
@ -16,6 +19,7 @@ public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHan
|
|||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
String returnTo = consumeReturnTo(request.getSession(false));
|
||||
if (exception instanceof AccountPendingException) {
|
||||
getRedirectStrategy().sendRedirect(request, response, "/pending-approval");
|
||||
return;
|
||||
|
|
@ -30,6 +34,24 @@ public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHan
|
|||
return;
|
||||
}
|
||||
|
||||
if (returnTo != null) {
|
||||
getRedirectStrategy().sendRedirect(
|
||||
request,
|
||||
response,
|
||||
"/login?returnTo=" + URLEncoder.encode(returnTo, StandardCharsets.UTF_8)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
super.onAuthenticationFailure(request, response, exception);
|
||||
}
|
||||
|
||||
private String consumeReturnTo(HttpSession session) {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = session.getAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE);
|
||||
session.removeAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE);
|
||||
return value instanceof String str ? OAuthLoginRedirectSupport.sanitizeReturnTo(str) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
|
|
@ -15,7 +16,7 @@ import java.io.IOException;
|
|||
public class OAuth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
public OAuth2LoginSuccessHandler() {
|
||||
setDefaultTargetUrl("/");
|
||||
setDefaultTargetUrl(OAuthLoginRedirectSupport.DEFAULT_TARGET_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -27,6 +28,21 @@ public class OAuth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSu
|
|||
request.getSession().setAttribute("platformPrincipal", principal);
|
||||
}
|
||||
}
|
||||
String returnTo = consumeReturnTo(request.getSession(false));
|
||||
if (returnTo != null) {
|
||||
getRedirectStrategy().sendRedirect(request, response, returnTo);
|
||||
clearAuthenticationAttributes(request);
|
||||
return;
|
||||
}
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
|
||||
private String consumeReturnTo(HttpSession session) {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = session.getAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE);
|
||||
session.removeAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE);
|
||||
return value instanceof String str ? OAuthLoginRedirectSupport.sanitizeReturnTo(str) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.iflytek.skillhub.auth.oauth;
|
||||
|
||||
public final class OAuthLoginRedirectSupport {
|
||||
|
||||
public static final String SESSION_RETURN_TO_ATTRIBUTE = "skillhub.oauth.returnTo";
|
||||
public static final String DEFAULT_TARGET_URL = "/dashboard";
|
||||
|
||||
private OAuthLoginRedirectSupport() {
|
||||
}
|
||||
|
||||
public static String sanitizeReturnTo(String candidate) {
|
||||
if (candidate == null || candidate.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = candidate.trim();
|
||||
if (!trimmed.startsWith("/") || trimmed.startsWith("//")) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed.contains("\r") || trimmed.contains("\n")) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.iflytek.skillhub.auth.oauth;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SkillHubOAuth2AuthorizationRequestResolver
|
||||
implements org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver {
|
||||
|
||||
private final DefaultOAuth2AuthorizationRequestResolver delegate;
|
||||
|
||||
public SkillHubOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
|
||||
this.delegate = new DefaultOAuth2AuthorizationRequestResolver(
|
||||
clientRegistrationRepository,
|
||||
"/oauth2/authorization"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
|
||||
OAuth2AuthorizationRequest authorizationRequest = delegate.resolve(request);
|
||||
rememberReturnTo(request);
|
||||
return authorizationRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
|
||||
OAuth2AuthorizationRequest authorizationRequest = delegate.resolve(request, clientRegistrationId);
|
||||
rememberReturnTo(request);
|
||||
return authorizationRequest;
|
||||
}
|
||||
|
||||
private void rememberReturnTo(HttpServletRequest request) {
|
||||
String returnTo = OAuthLoginRedirectSupport.sanitizeReturnTo(request.getParameter("returnTo"));
|
||||
if (returnTo == null) {
|
||||
request.getSession().removeAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE);
|
||||
return;
|
||||
}
|
||||
request.getSession().setAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE, returnTo);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.iflytek.skillhub.auth.oauth;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OAuth2AuthorizationRequestResolverTest {
|
||||
|
||||
private SkillHubOAuth2AuthorizationRequestResolver resolver;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ClientRegistration github = ClientRegistration.withRegistrationId("github")
|
||||
.clientId("client")
|
||||
.clientSecret("secret")
|
||||
.authorizationUri("https://example.test/oauth/authorize")
|
||||
.tokenUri("https://example.test/oauth/token")
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
|
||||
.userInfoUri("https://example.test/user")
|
||||
.userNameAttributeName("id")
|
||||
.authorizationGrantType(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.scope("read:user")
|
||||
.clientName("GitHub")
|
||||
.build();
|
||||
resolver = new SkillHubOAuth2AuthorizationRequestResolver(new InMemoryClientRegistrationRepository(github));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_storesSanitizedReturnToInSession() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/oauth2/authorization/github");
|
||||
request.setParameter("returnTo", "/dashboard/publish?draft=1");
|
||||
|
||||
resolver.resolve(request, "github");
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.getAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE))
|
||||
.isEqualTo("/dashboard/publish?draft=1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_ignoresUnsafeReturnTo() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/oauth2/authorization/github");
|
||||
request.setParameter("returnTo", "https://evil.example");
|
||||
|
||||
resolver.resolve(request, "github");
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.getAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE)).isNull();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.iflytek.skillhub.auth.oauth;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OAuth2LoginHandlersTest {
|
||||
|
||||
@Test
|
||||
void successHandler_redirectsToStoredReturnTo() throws Exception {
|
||||
OAuth2LoginSuccessHandler handler = new OAuth2LoginSuccessHandler();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpSession session = request.getSession(true);
|
||||
session.setAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE, "/dashboard/publish");
|
||||
|
||||
var principal = new com.iflytek.skillhub.auth.rbac.PlatformPrincipal(
|
||||
"user-1", "User", "user@example.com", null, "github", Set.of()
|
||||
);
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
new DefaultOAuth2User(List.of(), Map.of("platformPrincipal", principal, "login", "user"), "login"),
|
||||
null,
|
||||
List.of()
|
||||
);
|
||||
|
||||
handler.onAuthenticationSuccess(request, response, authentication);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("/dashboard/publish");
|
||||
assertThat(session.getAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void failureHandler_redirectsBackToLoginWithReturnTo() throws Exception {
|
||||
OAuth2LoginFailureHandler handler = new OAuth2LoginFailureHandler();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpSession session = request.getSession(true);
|
||||
session.setAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE, "/settings/accounts");
|
||||
|
||||
handler.onAuthenticationFailure(
|
||||
request,
|
||||
response,
|
||||
new OAuth2AuthenticationException(new OAuth2Error("invalid_request"))
|
||||
);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("/login?returnTo=%2Fsettings%2Faccounts");
|
||||
assertThat(session.getAttribute(OAuthLoginRedirectSupport.SESSION_RETURN_TO_ATTRIBUTE)).isNull();
|
||||
}
|
||||
}
|
||||
|
|
@ -137,8 +137,11 @@ export async function getCurrentUser(): Promise<User | null> {
|
|||
export const authApi = {
|
||||
getMe: getCurrentUser,
|
||||
|
||||
async getProviders(): Promise<OAuthProvider[]> {
|
||||
const providers = await unwrap<OAuthProvider[]>(client.GET('/api/v1/auth/providers') as never)
|
||||
async getProviders(returnTo?: string): Promise<OAuthProvider[]> {
|
||||
const params = returnTo
|
||||
? { query: { returnTo } }
|
||||
: undefined
|
||||
const providers = await unwrap<OAuthProvider[]>(client.GET('/api/v1/auth/providers', params as never) as never)
|
||||
return providers
|
||||
.filter((provider) => provider.id && provider.name && provider.authorizationUrl)
|
||||
.map((provider) => ({
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ import { authApi } from '@/api/client'
|
|||
import { Button } from '@/shared/ui/button'
|
||||
import type { OAuthProvider } from '@/api/types'
|
||||
|
||||
export function LoginButton() {
|
||||
interface LoginButtonProps {
|
||||
returnTo?: string
|
||||
}
|
||||
|
||||
export function LoginButton({ returnTo }: LoginButtonProps) {
|
||||
const { data, isLoading } = useQuery<OAuthProvider[]>({
|
||||
queryKey: ['auth', 'providers'],
|
||||
queryFn: authApi.getProviders,
|
||||
queryKey: ['auth', 'providers', returnTo ?? ''],
|
||||
queryFn: () => authApi.getProviders(returnTo),
|
||||
})
|
||||
|
||||
const providers = data ?? []
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export function LoginPage() {
|
|||
<p className="text-sm text-muted-foreground">
|
||||
使用 GitHub 登录时,认证完成后会自动返回当前站点。
|
||||
</p>
|
||||
<LoginButton />
|
||||
<LoginButton returnTo={returnTo} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export function RegisterPage() {
|
|||
<p className="text-sm text-muted-foreground">
|
||||
直接使用现有 OAuth 账户进入平台,无需再创建本地密码。
|
||||
</p>
|
||||
<LoginButton />
|
||||
<LoginButton returnTo={returnTo} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue