diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js index b31f301d9..40ce83a75 100644 --- a/cypress/e2e/login.cy.js +++ b/cypress/e2e/login.cy.js @@ -40,18 +40,4 @@ describe('Login page', () => { .should('be.visible') .and('contain', 'You entered an invalid username or password...'); }); - - describe('OIDC login button', () => { - it('should exist', () => { - cy.get('[data-test="oidc-login"]').should('exist'); - }); - - // Validates that OIDC is configured correctly - it('should redirect to /oidc', () => { - // Set intercept first, since redirect on click can be quick - cy.intercept('GET', '/api/auth/oidc').as('oidcRedirect'); - cy.get('[data-test="oidc-login"]').click(); - cy.wait('@oidcRedirect'); - }); - }); }); diff --git a/src/service/routes/auth.js b/src/service/routes/auth.js index e6163c774..cba2fca69 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.js @@ -66,6 +66,18 @@ const loginSuccessHandler = () => async (req, res) => { } }; +router.get('/config', (req, res) => { + const usernamePasswordMethod = getLoginStrategy(); + res.send({ + // enabled username /password auth method + usernamePasswordMethod: usernamePasswordMethod, + // other enabled auth methods + otherMethods: getAuthMethods() + .map((am) => am.type.toLowerCase()) + .filter((authType) => authType !== usernamePasswordMethod), + }); +}); + // TODO: provide separate auth endpoints for each auth strategy or chain compatibile auth strategies // TODO: if providing separate auth methods, inform the frontend so it has relevant UI elements and appropriate client-side behavior router.post( @@ -82,9 +94,9 @@ router.post( loginSuccessHandler(), ); -router.get('/oidc', passport.authenticate(authStrategies['openidconnect'].type)); +router.get('/openidconnect', passport.authenticate(authStrategies['openidconnect'].type)); -router.get('/oidc/callback', (req, res, next) => { +router.get('/openidconnect/callback', (req, res, next) => { passport.authenticate(authStrategies['openidconnect'].type, (err, user, info) => { if (err) { console.error('Authentication error:', err); diff --git a/src/ui/services/auth.js b/src/ui/services/auth.js index 8dc39a2a7..498001032 100644 --- a/src/ui/services/auth.js +++ b/src/ui/services/auth.js @@ -32,7 +32,7 @@ export const getAxiosConfig = () => { return { withCredentials: true, headers: { - 'X-CSRF-TOKEN': getCookie('csrf'), + 'X-CSRF-TOKEN': getCookie('csrf') || '', Authorization: jwtToken ? `Bearer ${jwtToken}` : undefined, }, }; @@ -43,9 +43,9 @@ export const getAxiosConfig = () => { * @param {Object} error - The error object * @return {string} The error message */ -export const processAuthError = (error) => { +export const processAuthError = (error, jwtAuthEnabled = false) => { let errorMessage = `Failed to authorize user: ${error.response.data.trim()}. `; - if (!localStorage.getItem('ui_jwt_token')) { + if (jwtAuthEnabled && !localStorage.getItem('ui_jwt_token')) { errorMessage += 'Set your JWT token in the settings page or disable JWT auth in your app configuration.'; } else { diff --git a/src/ui/views/Login/Login.tsx b/src/ui/views/Login/Login.tsx index 31ed3ea15..7a4ecabfb 100644 --- a/src/ui/views/Login/Login.tsx +++ b/src/ui/views/Login/Login.tsx @@ -1,4 +1,4 @@ -import React, { useState, FormEvent } from 'react'; +import React, { useState, FormEvent, useEffect } from 'react'; import { useNavigate, Navigate } from 'react-router-dom'; import FormControl from '@material-ui/core/FormControl'; import InputLabel from '@material-ui/core/InputLabel'; @@ -12,10 +12,10 @@ import CardBody from '../../components/Card/CardBody'; import CardFooter from '../../components/Card/CardFooter'; import axios, { AxiosError } from 'axios'; import logo from '../../assets/img/git-proxy.png'; -import { Badge, CircularProgress, Snackbar } from '@material-ui/core'; -import { getCookie } from '../../utils'; +import { Badge, CircularProgress, FormLabel, Snackbar } from '@material-ui/core'; import { useAuth } from '../../auth/AuthProvider'; import { API_BASE } from '../../apiBase'; +import { getAxiosConfig, processAuthError } from '../../services/auth'; interface LoginResponse { username: string; @@ -34,6 +34,23 @@ const Login: React.FC = () => { const [success, setSuccess] = useState(false); const [gitAccountError, setGitAccountError] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [authMethods, setAuthMethods] = useState([]); + const [usernamePasswordMethod, setUsernamePasswordMethod] = useState(''); + + useEffect(() => { + axios.get(`${API_BASE}/api/auth/config`).then((response) => { + const usernamePasswordMethod = response.data.usernamePasswordMethod; + const otherMethods = response.data.otherMethods; + + setUsernamePasswordMethod(usernamePasswordMethod); + setAuthMethods(otherMethods); + + // Automatically login if only one non-username/password method is enabled + if (!usernamePasswordMethod && otherMethods.length === 1) { + handleAuthMethodLogin(otherMethods[0]); + } + }); + }, []); function validateForm(): boolean { return ( @@ -41,8 +58,8 @@ const Login: React.FC = () => { ); } - function handleOIDCLogin(): void { - window.location.href = `${API_BASE}/api/auth/oidc`; + function handleAuthMethodLogin(authMethod: string): void { + window.location.href = `${API_BASE}/api/auth/${authMethod}`; } function handleSubmit(event: FormEvent): void { @@ -50,17 +67,7 @@ const Login: React.FC = () => { setIsLoading(true); axios - .post( - loginUrl, - { username, password }, - { - withCredentials: true, - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': getCookie('csrf') || '', - }, - }, - ) + .post(loginUrl, { username, password }, getAxiosConfig()) .then(() => { window.sessionStorage.setItem('git.proxy.login', 'success'); setMessage('Success!'); @@ -72,7 +79,7 @@ const Login: React.FC = () => { window.sessionStorage.setItem('git.proxy.login', 'success'); setGitAccountError(true); } else if (error.response?.status === 403) { - setMessage('You do not have the correct access permissions...'); + setMessage(processAuthError(error, false)); } else { setMessage('You entered an invalid username or password...'); } @@ -113,52 +120,80 @@ const Login: React.FC = () => { /> - - - - - Username - setUsername(e.target.value)} - autoFocus - data-test='username' - /> - - - - - - - Password - setPassword(e.target.value)} - data-test='password' - /> - - - - - + {usernamePasswordMethod ? ( + + + + + Login + + + Username + setUsername(e.target.value)} + autoFocus + data-test='username' + /> + + + + + + + Password + setPassword(e.target.value)} + data-test='password' + /> + + + + + ) : ( + + + Username/password authentication is not enabled at this time. + + + )} + {/* Show login buttons if available (one on top of the other) */} + {!isLoading ? ( <> - - + {usernamePasswordMethod && ( + + )} + {authMethods.map((am) => ( + + ))} ) : (
@@ -168,7 +203,12 @@ const Login: React.FC = () => {
- + View our open source activity feed or{' '} scroll through projects we contribute to