Anonymous
Лучший способ выполнить аутентификацию Oauth2 с помощью Java Spring + React Native
Сообщение
Anonymous » 19 янв 2026, 20:00
У меня возникли проблемы с созданием конечной точки в моем бэкэнде /login, которая должна аутентифицировать пользователя с помощью Github Oauth2. Фронтенд делается на React Native, и я пытаюсь это сделать, но не могу
Код: Выделить всё
WebBrowser.maybeCompleteAuthSession();
const discovery = {
authorizationEndpoint: 'https://github.com/login/oauth/authorize',
tokenEndpoint: 'https://github.com/login/oauth/access_token',
};
const scheme = 'coderpg';
const redirectUri = makeRedirectUri({ scheme });
const TOKEN_STORAGE_KEY = '@coderpg:token';
// Configurar interceptor do axios
axios.interceptors.request.use(
async (config) => {
const token = await AsyncStorage.getItem(TOKEN_STORAGE_KEY);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export function useAuth() {
const queryClient = useQueryClient();
const [request, response, promptAsync] = useAuthRequest(
{
clientId: ENV.GITHUB_CLIENT_ID,
scopes: ['user'],
redirectUri,
usePKCE: false,
},
discovery
);
const loginBackendMutation = useMutation({
mutationFn: async (code: string) => {
console.log('🚀 Enviando para o backend:', {
code: code.substring(0, 10) + '...',
redirectUri: redirectUri,
});
const response = await axios.post(`${ENV.API_BASE_URL}/auth/login`, {
code,
redirectUri: redirectUri,
});
console.log('Resposta do backend:', {
success: response.data?.success,
hasToken: !!response.data?.data?.token,
authenticated: response.data?.data?.authStatus?.authenticated,
});
return response.data;
},
onSuccess: async (response) => {
const token = response.data?.token;
if (token) {
await AsyncStorage.setItem(TOKEN_STORAGE_KEY, token);
console.log('💾 Token salvo com sucesso:', token.substring(0, 8) + '...');
}
// Invalida queries para recarregar status
queryClient.invalidateQueries({ queryKey: ['authStatus'] });
},
onError: (error: any) => {
console.error('Erro ao logar no backend:', {
message: error.message,
response: error.response?.data,
status: error.response?.status,
});
}
});
// Query para buscar status de autenticação
const { data: authStatus, isLoading, refetch } = useQuery({
queryKey: ['authStatus'],
queryFn: async () => {
const token = await AsyncStorage.getItem(TOKEN_STORAGE_KEY);
if (!token) {
console.log('⚠️ Sem token salvo no storage');
return { authenticated: false };
}
console.log('🔍 Verificando status com token:', token.substring(0, 8) + '...');
try {
const status = await getAuthStatusUseCase.execute();
console.log('✅ Status recebido:', {
authenticated: status.authenticated,
userId: status.userId,
needsOnBoarding: status.needsOnBoarding,
});
return status;
} catch (error: any) {
console.error('❌ Erro ao buscar status:', error.message);
// Se der 401, remove o token inválido
if (error.response?.status === 401) {
console.log('🗑️ Token inválido, removendo...');
await AsyncStorage.removeItem(TOKEN_STORAGE_KEY);
}
return { authenticated: false };
}
},
retry: false,
enabled: true, // ← IMPORTANTE: Sempre habilitado
});
const logoutMutation = useMutation({
mutationFn: async () => {
console.log('👋 Fazendo logout...');
await logoutUseCase.execute();
await AsyncStorage.removeItem(TOKEN_STORAGE_KEY);
},
onSuccess: () => {
console.log('✅ Logout realizado com sucesso');
queryClient.invalidateQueries({ queryKey: ['authStatus'] });
queryClient.clear();
},
});
// Efeito para processar resposta do OAuth
useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
console.log('✅ Código OAuth recebido do GitHub');
loginBackendMutation.mutate(code);
} else if (response?.type === 'error') {
console.error('❌ Erro no AuthSession:', response.error);
} else if (response?.type === 'cancel') {
console.log('⚠️ Login cancelado pelo usuário');
}
}, [response]);
const login = async () => {
console.log('🔐 Iniciando login com GitHub...');
console.log('📍 Redirect URI:', redirectUri);
await promptAsync();
};
const finalAuthStatus = loginBackendMutation.data?.data?.authStatus || authStatus;
const isAuthenticated = finalAuthStatus?.authenticated || false;
console.log('📊 Estado final:', {
isAuthenticated,
isLoading: isLoading || loginBackendMutation.isPending,
needsOnBoarding: finalAuthStatus?.needsOnBoarding,
});
return {
authStatus: finalAuthStatus,
isLoading: isLoading || loginBackendMutation.isPending,
isAuthenticated,
loginData: loginBackendMutation.data?.data,
login,
logout: () => logoutMutation.mutateAsync(),
isLoggingOut: logoutMutation.isPending,
refetchAuthStatus: refetch,
};
}
e o endpoint:
@PostMapping(AuthRoute.LOGIN)
public ResponseEntity authenticateWithGithub(
@Valid @RequestBody MobileAuthRequest request
) {
log.info("Mobile login attempt - redirectUri: {}", request.getRedirectUri());
Map tokenResponse = mobileAuthService
.exchangeCodeForToken(request.getCode(), request.getRedirectUri());
String accessToken = (String) tokenResponse.get("access_token");
String refreshToken = (String) tokenResponse.get("refresh_token");
if (accessToken == null || accessToken.isBlank()) {
throw new BusinessException("Failed to get access token from GitHub");
}
Map githubUser = mobileAuthService.getGitHubUser(accessToken);
Long githubId = ((Number) githubUser.get("id")).longValue();
String githubUsername = (String) githubUser.get("login");
String email = (String) githubUser.get("email");
String avatarUrl = (String) githubUser.get("avatar_url");
String bio = (String) githubUser.get("bio");
String location = (String) githubUser.get("location");
String website = (String) githubUser.get("blog");
Integer publicRepos = (Integer) githubUser.get("public_repos");
Integer followers = (Integer) githubUser.get("followers");
Integer following = (Integer) githubUser.get("following");
String createdAt = (String) githubUser.get("created_at");
Optional existingUser = userRepository.findByGitHubId(githubId);
User user;
boolean isNewUser = false;
if (existingUser.isPresent()) {
user = existingUser.get();
user.setGithubUsername(githubUsername);
user.setAvatarUrl(avatarUrl);
user.setBio(bio);
user.setLocation(location);
user.setWebsite(website);
user.setGithubPublicRepos(publicRepos);
user.setGithubFollowers(followers);
user.setGithubFollowing(following);
user.setLastSyncAt(LocalDateTime.now());
user = userRepository.save(user);
log.info("Existing user logged in: {} ({})", user.getGithubUsername(), user.getId());
} else {
isNewUser = true;
User newUser = User.builder()
.githubId(githubId)
.githubUsername(githubUsername)
.name(null)
.email(email)
.avatarUrl(avatarUrl)
.bio(bio)
.location(location)
.website(website)
.classType(null)
.githubPublicRepos(publicRepos)
.githubFollowers(followers)
.githubFollowing(following)
.githubCreatedAt(createdAt != null ?
LocalDateTime.parse(createdAt.replace("Z", "")) : null)
.active(true)
.build();
Context createUserContext = new Context(newUser);
user = createUserPort.execute(createUserContext);
log.info("New user created: {} ({})", user.getGithubUsername(), user.getId());
}
LocalDateTime expiresAt = mobileAuthService.calculateExpiresAt(tokenResponse.get("expires_in"));
Context saveTokenContext = new Context();
saveTokenContext.putProperty("userId", user.getId());
saveTokenContext.putProperty("accessToken", accessToken);
saveTokenContext.putProperty("refreshToken", refreshToken);
saveTokenContext.putProperty("expiresAt", expiresAt);
saveGitHubTokenPort.execute(saveTokenContext);
boolean needsOnBoarding = user.getName() == null || user.getClassType() == null;
AuthStatusResponse authStatus = AuthStatusResponse.builder()
.authenticated(true)
.userId(user.getId())
.githubUsername(user.getGithubUsername())
.name(user.getName())
.avatarUrl(user.getAvatarUrl())
.hasValidGitHubToken(true)
.needsOnBoarding(needsOnBoarding)
.build();
LoginResponse loginResponse = LoginResponse.builder()
.token(user.getId().toString())
.authStatus(authStatus)
.build();
return ResponseEntity
.status(isNewUser ? HttpStatus.CREATED : HttpStatus.OK)
.body(ApiResponse.success(loginResponse));
}
package samukadev.coderpg.security.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import samukadev.coderpg.core.persistence.UserRepositoryPort;
import samukadev.coderpg.domain.User;
import samukadev.coderpg.security.oauth.GitHubOAuthService;
import java.io.IOException;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Component
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final UserRepositoryPort userRepository;
private final GitHubOAuthService gitHubOAuthService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String requestPath = request.getRequestURI();
try {
String token = extractTokenFromRequest(request);
if (token != null && !token.isBlank()) {
log.debug("🔑 Token encontrado para {}: {}", requestPath, token.substring(0, 8) + "...");
authenticateUser(token, request);
} else {
log.debug("⚠️ Nenhum token encontrado para {}", requestPath);
}
} catch (Exception e) {
log.error("❌ Error authenticating user from token for {}: {}", requestPath, e.getMessage());
}
filterChain.doFilter(request, response);
}
private String extractTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
private void authenticateUser(String userId, HttpServletRequest request) {
try {
log.debug("🔍 Tentando autenticar usuário com ID: {}", userId.substring(0, 8) + "...");
UUID userUuid = UUID.fromString(userId);
Optional userOpt = userRepository.get(userUuid);
if (userOpt.isEmpty()) {
log.warn("⚠️ User not found for ID: {}", userId.substring(0, 8) + "...");
return;
}
User user = userOpt.get();
log.debug("✅ Usuário encontrado: {} (githubId: {})", user.getGithubUsername(), user.getGithubId());
boolean hasValidToken = gitHubOAuthService.hasValidToken(user.getId());
if (!hasValidToken) {
log.warn("⚠️ User {} does not have a valid GitHub token", userId.substring(0, 8) + "...");
return;
}
log.debug("✅ Token do GitHub válido para usuário {}", user.getGithubUsername());
// IMPORTANTE: Coloca o githubId como "name" da autenticação
// O AuthController usa authentication.getName() para buscar o user
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user.getGithubId().toString(), // ← githubId aqui
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("✅ User {} authenticated successfully via token", userId.substring(0, 8) + "...");
} catch (IllegalArgumentException e) {
log.warn("⚠️ Invalid UUID format: {}", userId);
} catch (Exception e) {
log.error("❌ Error during authentication: {}", e.getMessage(), e);
}
}
}
Я знаю, это похоже на код ИИ, потому что это действительно так. Я работаю над этим уже много часов и до сих пор не могу, мне нужно было использовать ИИ, пока не сойду с ума.
Если вы знаете какой-нибудь способ сделать это, помогите мне, пожалуйста.
если вы хотите увидеть репозитории, вот они:
https://github.com/franklin-samuel/CodeRPG
https://github.com/franklin-samuel/CodeRPG-Mobile
Подробнее здесь:
https://stackoverflow.com/questions/798 ... act-native
1768842016
Anonymous
У меня возникли проблемы с созданием конечной точки в моем бэкэнде /login, которая должна аутентифицировать пользователя с помощью Github Oauth2. Фронтенд делается на React Native, и я пытаюсь это сделать, но не могу [code]WebBrowser.maybeCompleteAuthSession(); const discovery = { authorizationEndpoint: 'https://github.com/login/oauth/authorize', tokenEndpoint: 'https://github.com/login/oauth/access_token', }; const scheme = 'coderpg'; const redirectUri = makeRedirectUri({ scheme }); const TOKEN_STORAGE_KEY = '@coderpg:token'; // Configurar interceptor do axios axios.interceptors.request.use( async (config) => { const token = await AsyncStorage.getItem(TOKEN_STORAGE_KEY); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); export function useAuth() { const queryClient = useQueryClient(); const [request, response, promptAsync] = useAuthRequest( { clientId: ENV.GITHUB_CLIENT_ID, scopes: ['user'], redirectUri, usePKCE: false, }, discovery ); const loginBackendMutation = useMutation({ mutationFn: async (code: string) => { console.log('🚀 Enviando para o backend:', { code: code.substring(0, 10) + '...', redirectUri: redirectUri, }); const response = await axios.post(`${ENV.API_BASE_URL}/auth/login`, { code, redirectUri: redirectUri, }); console.log('Resposta do backend:', { success: response.data?.success, hasToken: !!response.data?.data?.token, authenticated: response.data?.data?.authStatus?.authenticated, }); return response.data; }, onSuccess: async (response) => { const token = response.data?.token; if (token) { await AsyncStorage.setItem(TOKEN_STORAGE_KEY, token); console.log('💾 Token salvo com sucesso:', token.substring(0, 8) + '...'); } // Invalida queries para recarregar status queryClient.invalidateQueries({ queryKey: ['authStatus'] }); }, onError: (error: any) => { console.error('Erro ao logar no backend:', { message: error.message, response: error.response?.data, status: error.response?.status, }); } }); // Query para buscar status de autenticação const { data: authStatus, isLoading, refetch } = useQuery({ queryKey: ['authStatus'], queryFn: async () => { const token = await AsyncStorage.getItem(TOKEN_STORAGE_KEY); if (!token) { console.log('⚠️ Sem token salvo no storage'); return { authenticated: false }; } console.log('🔍 Verificando status com token:', token.substring(0, 8) + '...'); try { const status = await getAuthStatusUseCase.execute(); console.log('✅ Status recebido:', { authenticated: status.authenticated, userId: status.userId, needsOnBoarding: status.needsOnBoarding, }); return status; } catch (error: any) { console.error('❌ Erro ao buscar status:', error.message); // Se der 401, remove o token inválido if (error.response?.status === 401) { console.log('🗑️ Token inválido, removendo...'); await AsyncStorage.removeItem(TOKEN_STORAGE_KEY); } return { authenticated: false }; } }, retry: false, enabled: true, // ← IMPORTANTE: Sempre habilitado }); const logoutMutation = useMutation({ mutationFn: async () => { console.log('👋 Fazendo logout...'); await logoutUseCase.execute(); await AsyncStorage.removeItem(TOKEN_STORAGE_KEY); }, onSuccess: () => { console.log('✅ Logout realizado com sucesso'); queryClient.invalidateQueries({ queryKey: ['authStatus'] }); queryClient.clear(); }, }); // Efeito para processar resposta do OAuth useEffect(() => { if (response?.type === 'success') { const { code } = response.params; console.log('✅ Código OAuth recebido do GitHub'); loginBackendMutation.mutate(code); } else if (response?.type === 'error') { console.error('❌ Erro no AuthSession:', response.error); } else if (response?.type === 'cancel') { console.log('⚠️ Login cancelado pelo usuário'); } }, [response]); const login = async () => { console.log('🔐 Iniciando login com GitHub...'); console.log('📍 Redirect URI:', redirectUri); await promptAsync(); }; const finalAuthStatus = loginBackendMutation.data?.data?.authStatus || authStatus; const isAuthenticated = finalAuthStatus?.authenticated || false; console.log('📊 Estado final:', { isAuthenticated, isLoading: isLoading || loginBackendMutation.isPending, needsOnBoarding: finalAuthStatus?.needsOnBoarding, }); return { authStatus: finalAuthStatus, isLoading: isLoading || loginBackendMutation.isPending, isAuthenticated, loginData: loginBackendMutation.data?.data, login, logout: () => logoutMutation.mutateAsync(), isLoggingOut: logoutMutation.isPending, refetchAuthStatus: refetch, }; } e o endpoint: @PostMapping(AuthRoute.LOGIN) public ResponseEntity authenticateWithGithub( @Valid @RequestBody MobileAuthRequest request ) { log.info("Mobile login attempt - redirectUri: {}", request.getRedirectUri()); Map tokenResponse = mobileAuthService .exchangeCodeForToken(request.getCode(), request.getRedirectUri()); String accessToken = (String) tokenResponse.get("access_token"); String refreshToken = (String) tokenResponse.get("refresh_token"); if (accessToken == null || accessToken.isBlank()) { throw new BusinessException("Failed to get access token from GitHub"); } Map githubUser = mobileAuthService.getGitHubUser(accessToken); Long githubId = ((Number) githubUser.get("id")).longValue(); String githubUsername = (String) githubUser.get("login"); String email = (String) githubUser.get("email"); String avatarUrl = (String) githubUser.get("avatar_url"); String bio = (String) githubUser.get("bio"); String location = (String) githubUser.get("location"); String website = (String) githubUser.get("blog"); Integer publicRepos = (Integer) githubUser.get("public_repos"); Integer followers = (Integer) githubUser.get("followers"); Integer following = (Integer) githubUser.get("following"); String createdAt = (String) githubUser.get("created_at"); Optional existingUser = userRepository.findByGitHubId(githubId); User user; boolean isNewUser = false; if (existingUser.isPresent()) { user = existingUser.get(); user.setGithubUsername(githubUsername); user.setAvatarUrl(avatarUrl); user.setBio(bio); user.setLocation(location); user.setWebsite(website); user.setGithubPublicRepos(publicRepos); user.setGithubFollowers(followers); user.setGithubFollowing(following); user.setLastSyncAt(LocalDateTime.now()); user = userRepository.save(user); log.info("Existing user logged in: {} ({})", user.getGithubUsername(), user.getId()); } else { isNewUser = true; User newUser = User.builder() .githubId(githubId) .githubUsername(githubUsername) .name(null) .email(email) .avatarUrl(avatarUrl) .bio(bio) .location(location) .website(website) .classType(null) .githubPublicRepos(publicRepos) .githubFollowers(followers) .githubFollowing(following) .githubCreatedAt(createdAt != null ? LocalDateTime.parse(createdAt.replace("Z", "")) : null) .active(true) .build(); Context createUserContext = new Context(newUser); user = createUserPort.execute(createUserContext); log.info("New user created: {} ({})", user.getGithubUsername(), user.getId()); } LocalDateTime expiresAt = mobileAuthService.calculateExpiresAt(tokenResponse.get("expires_in")); Context saveTokenContext = new Context(); saveTokenContext.putProperty("userId", user.getId()); saveTokenContext.putProperty("accessToken", accessToken); saveTokenContext.putProperty("refreshToken", refreshToken); saveTokenContext.putProperty("expiresAt", expiresAt); saveGitHubTokenPort.execute(saveTokenContext); boolean needsOnBoarding = user.getName() == null || user.getClassType() == null; AuthStatusResponse authStatus = AuthStatusResponse.builder() .authenticated(true) .userId(user.getId()) .githubUsername(user.getGithubUsername()) .name(user.getName()) .avatarUrl(user.getAvatarUrl()) .hasValidGitHubToken(true) .needsOnBoarding(needsOnBoarding) .build(); LoginResponse loginResponse = LoginResponse.builder() .token(user.getId().toString()) .authStatus(authStatus) .build(); return ResponseEntity .status(isNewUser ? HttpStatus.CREATED : HttpStatus.OK) .body(ApiResponse.success(loginResponse)); } package samukadev.coderpg.security.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import samukadev.coderpg.core.persistence.UserRepositoryPort; import samukadev.coderpg.domain.User; import samukadev.coderpg.security.oauth.GitHubOAuthService; import java.io.IOException; import java.util.Collections; import java.util.Optional; import java.util.UUID; @Slf4j @Component @RequiredArgsConstructor public class TokenAuthenticationFilter extends OncePerRequestFilter { private final UserRepositoryPort userRepository; private final GitHubOAuthService gitHubOAuthService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { String requestPath = request.getRequestURI(); try { String token = extractTokenFromRequest(request); if (token != null && !token.isBlank()) { log.debug("🔑 Token encontrado para {}: {}", requestPath, token.substring(0, 8) + "..."); authenticateUser(token, request); } else { log.debug("⚠️ Nenhum token encontrado para {}", requestPath); } } catch (Exception e) { log.error("❌ Error authenticating user from token for {}: {}", requestPath, e.getMessage()); } filterChain.doFilter(request, response); } private String extractTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } private void authenticateUser(String userId, HttpServletRequest request) { try { log.debug("🔍 Tentando autenticar usuário com ID: {}", userId.substring(0, 8) + "..."); UUID userUuid = UUID.fromString(userId); Optional userOpt = userRepository.get(userUuid); if (userOpt.isEmpty()) { log.warn("⚠️ User not found for ID: {}", userId.substring(0, 8) + "..."); return; } User user = userOpt.get(); log.debug("✅ Usuário encontrado: {} (githubId: {})", user.getGithubUsername(), user.getGithubId()); boolean hasValidToken = gitHubOAuthService.hasValidToken(user.getId()); if (!hasValidToken) { log.warn("⚠️ User {} does not have a valid GitHub token", userId.substring(0, 8) + "..."); return; } log.debug("✅ Token do GitHub válido para usuário {}", user.getGithubUsername()); // IMPORTANTE: Coloca o githubId como "name" da autenticação // O AuthController usa authentication.getName() para buscar o user UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( user.getGithubId().toString(), // ← githubId aqui null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); log.info("✅ User {} authenticated successfully via token", userId.substring(0, 8) + "..."); } catch (IllegalArgumentException e) { log.warn("⚠️ Invalid UUID format: {}", userId); } catch (Exception e) { log.error("❌ Error during authentication: {}", e.getMessage(), e); } } } [/code] Я знаю, это похоже на код ИИ, потому что это действительно так. Я работаю над этим уже много часов и до сих пор не могу, мне нужно было использовать ИИ, пока не сойду с ума. Если вы знаете какой-нибудь способ сделать это, помогите мне, пожалуйста. если вы хотите увидеть репозитории, вот они: https://github.com/franklin-samuel/CodeRPG https://github.com/franklin-samuel/CodeRPG-Mobile Подробнее здесь: [url]https://stackoverflow.com/questions/79871367/best-way-to-do-an-oauth2-authentication-with-java-spring-react-native[/url]