Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions cypress/e2e/login.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
16 changes: 14 additions & 2 deletions src/service/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/ui/services/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand All @@ -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 {
Expand Down
164 changes: 102 additions & 62 deletions src/ui/views/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -34,33 +34,40 @@ const Login: React.FC = () => {
const [success, setSuccess] = useState<boolean>(false);
const [gitAccountError, setGitAccountError] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [authMethods, setAuthMethods] = useState<string[]>([]);
const [usernamePasswordMethod, setUsernamePasswordMethod] = useState<string>('');

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 (
username.length > 0 && username.length < 100 && password.length > 0 && password.length < 200
);
}

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 {
event.preventDefault();
setIsLoading(true);

axios
.post<LoginResponse>(
loginUrl,
{ username, password },
{
withCredentials: true,
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCookie('csrf') || '',
},
},
)
.post<LoginResponse>(loginUrl, { username, password }, getAxiosConfig())
.then(() => {
window.sessionStorage.setItem('git.proxy.login', 'success');
setMessage('Success!');
Expand All @@ -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...');
}
Expand Down Expand Up @@ -113,52 +120,80 @@ const Login: React.FC = () => {
/>
</div>
</CardHeader>
<CardBody>
<GridContainer>
<GridItem xs={12} sm={12} md={12}>
<FormControl fullWidth>
<InputLabel htmlFor='username'>Username</InputLabel>
<Input
id='username'
type='text'
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
data-test='username'
/>
</FormControl>
</GridItem>
</GridContainer>
<GridContainer>
<GridItem xs={12} sm={12} md={12}>
<FormControl fullWidth>
<InputLabel htmlFor='password'>Password</InputLabel>
<Input
id='password'
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
data-test='password'
/>
</FormControl>
</GridItem>
</GridContainer>
</CardBody>
<CardFooter>
{usernamePasswordMethod ? (
<CardBody>
<GridContainer>
<GridItem xs={12} sm={12} md={12}>
<FormLabel component='legend' style={{ fontSize: '1.2rem', marginTop: 10 }}>
Login
</FormLabel>
<FormControl fullWidth>
<InputLabel htmlFor='username'>Username</InputLabel>
<Input
id='username'
type='text'
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
data-test='username'
/>
</FormControl>
</GridItem>
</GridContainer>
<GridContainer>
<GridItem xs={12} sm={12} md={12}>
<FormControl fullWidth>
<InputLabel htmlFor='password'>Password</InputLabel>
<Input
id='password'
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
data-test='password'
/>
</FormControl>
</GridItem>
</GridContainer>
</CardBody>
) : (
<CardBody>
<FormLabel
component='legend'
style={{ fontSize: '1rem', marginTop: 10, marginBottom: 0 }}
>
Username/password authentication is not enabled at this time.
</FormLabel>
</CardBody>
)}
{/* Show login buttons if available (one on top of the other) */}
<CardFooter style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{!isLoading ? (
<>
<Button
color='success'
block
disabled={!validateForm()}
type='submit'
data-test='login'
>
Login
</Button>
<Button color='warning' block onClick={handleOIDCLogin} data-test='oidc-login'>
Login with OIDC
</Button>
{usernamePasswordMethod && (
<Button
color='success'
block
disabled={!validateForm()}
type='submit'
data-test='login'
>
Login
</Button>
)}
{authMethods.map((am) => (
<Button
color='success'
block
onClick={() => handleAuthMethodLogin(am)}
data-test={`${am}-login`}
key={am}
>
Login
{authMethods.length > 1 || usernamePasswordMethod
? ` with ${am.toUpperCase()}`
: ''}
</Button>
))}
</>
) : (
<div style={{ textAlign: 'center', width: '100%', opacity: 0.5, color: 'green' }}>
Expand All @@ -168,7 +203,12 @@ const Login: React.FC = () => {
</CardFooter>
</Card>
<div style={{ textAlign: 'center', opacity: 0.9, fontSize: 12, marginTop: 20 }}>
<Badge overlap='rectangular' color='error' badgeContent='NEW' />
<Badge
overlap='rectangular'
color='error'
badgeContent='NEW'
style={{ marginRight: 20 }}
/>
<span style={{ paddingLeft: 20 }}>
View our <a href='/dashboard/push'>open source activity feed</a> or{' '}
<a href='/dashboard/repo'>scroll through projects</a> we contribute to
Expand Down
Loading