Skip to content

Commit b198b31

Browse files
authored
Merge pull request #1227 from jescalada/login-page-auth-options-flexibility
feat: improve login page flexibility
2 parents cc8fac5 + ffa904d commit b198b31

File tree

4 files changed

+119
-81
lines changed

4 files changed

+119
-81
lines changed

cypress/e2e/login.cy.js

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,4 @@ describe('Login page', () => {
4040
.should('be.visible')
4141
.and('contain', 'You entered an invalid username or password...');
4242
});
43-
44-
describe('OIDC login button', () => {
45-
it('should exist', () => {
46-
cy.get('[data-test="oidc-login"]').should('exist');
47-
});
48-
49-
// Validates that OIDC is configured correctly
50-
it('should redirect to /oidc', () => {
51-
// Set intercept first, since redirect on click can be quick
52-
cy.intercept('GET', '/api/auth/oidc').as('oidcRedirect');
53-
cy.get('[data-test="oidc-login"]').click();
54-
cy.wait('@oidcRedirect');
55-
});
56-
});
5743
});

src/service/routes/auth.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ const loginSuccessHandler = () => async (req, res) => {
6666
}
6767
};
6868

69+
router.get('/config', (req, res) => {
70+
const usernamePasswordMethod = getLoginStrategy();
71+
res.send({
72+
// enabled username /password auth method
73+
usernamePasswordMethod: usernamePasswordMethod,
74+
// other enabled auth methods
75+
otherMethods: getAuthMethods()
76+
.map((am) => am.type.toLowerCase())
77+
.filter((authType) => authType !== usernamePasswordMethod),
78+
});
79+
});
80+
6981
// TODO: provide separate auth endpoints for each auth strategy or chain compatibile auth strategies
7082
// TODO: if providing separate auth methods, inform the frontend so it has relevant UI elements and appropriate client-side behavior
7183
router.post(
@@ -82,9 +94,9 @@ router.post(
8294
loginSuccessHandler(),
8395
);
8496

85-
router.get('/oidc', passport.authenticate(authStrategies['openidconnect'].type));
97+
router.get('/openidconnect', passport.authenticate(authStrategies['openidconnect'].type));
8698

87-
router.get('/oidc/callback', (req, res, next) => {
99+
router.get('/openidconnect/callback', (req, res, next) => {
88100
passport.authenticate(authStrategies['openidconnect'].type, (err, user, info) => {
89101
if (err) {
90102
console.error('Authentication error:', err);

src/ui/services/auth.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const getAxiosConfig = () => {
3232
return {
3333
withCredentials: true,
3434
headers: {
35-
'X-CSRF-TOKEN': getCookie('csrf'),
35+
'X-CSRF-TOKEN': getCookie('csrf') || '',
3636
Authorization: jwtToken ? `Bearer ${jwtToken}` : undefined,
3737
},
3838
};
@@ -43,9 +43,9 @@ export const getAxiosConfig = () => {
4343
* @param {Object} error - The error object
4444
* @return {string} The error message
4545
*/
46-
export const processAuthError = (error) => {
46+
export const processAuthError = (error, jwtAuthEnabled = false) => {
4747
let errorMessage = `Failed to authorize user: ${error.response.data.trim()}. `;
48-
if (!localStorage.getItem('ui_jwt_token')) {
48+
if (jwtAuthEnabled && !localStorage.getItem('ui_jwt_token')) {
4949
errorMessage +=
5050
'Set your JWT token in the settings page or disable JWT auth in your app configuration.';
5151
} else {

src/ui/views/Login/Login.tsx

Lines changed: 102 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, FormEvent } from 'react';
1+
import React, { useState, FormEvent, useEffect } from 'react';
22
import { useNavigate, Navigate } from 'react-router-dom';
33
import FormControl from '@material-ui/core/FormControl';
44
import InputLabel from '@material-ui/core/InputLabel';
@@ -12,10 +12,10 @@ import CardBody from '../../components/Card/CardBody';
1212
import CardFooter from '../../components/Card/CardFooter';
1313
import axios, { AxiosError } from 'axios';
1414
import logo from '../../assets/img/git-proxy.png';
15-
import { Badge, CircularProgress, Snackbar } from '@material-ui/core';
16-
import { getCookie } from '../../utils';
15+
import { Badge, CircularProgress, FormLabel, Snackbar } from '@material-ui/core';
1716
import { useAuth } from '../../auth/AuthProvider';
1817
import { API_BASE } from '../../apiBase';
18+
import { getAxiosConfig, processAuthError } from '../../services/auth';
1919

2020
interface LoginResponse {
2121
username: string;
@@ -34,33 +34,40 @@ const Login: React.FC = () => {
3434
const [success, setSuccess] = useState<boolean>(false);
3535
const [gitAccountError, setGitAccountError] = useState<boolean>(false);
3636
const [isLoading, setIsLoading] = useState<boolean>(false);
37+
const [authMethods, setAuthMethods] = useState<string[]>([]);
38+
const [usernamePasswordMethod, setUsernamePasswordMethod] = useState<string>('');
39+
40+
useEffect(() => {
41+
axios.get(`${API_BASE}/api/auth/config`).then((response) => {
42+
const usernamePasswordMethod = response.data.usernamePasswordMethod;
43+
const otherMethods = response.data.otherMethods;
44+
45+
setUsernamePasswordMethod(usernamePasswordMethod);
46+
setAuthMethods(otherMethods);
47+
48+
// Automatically login if only one non-username/password method is enabled
49+
if (!usernamePasswordMethod && otherMethods.length === 1) {
50+
handleAuthMethodLogin(otherMethods[0]);
51+
}
52+
});
53+
}, []);
3754

3855
function validateForm(): boolean {
3956
return (
4057
username.length > 0 && username.length < 100 && password.length > 0 && password.length < 200
4158
);
4259
}
4360

44-
function handleOIDCLogin(): void {
45-
window.location.href = `${API_BASE}/api/auth/oidc`;
61+
function handleAuthMethodLogin(authMethod: string): void {
62+
window.location.href = `${API_BASE}/api/auth/${authMethod}`;
4663
}
4764

4865
function handleSubmit(event: FormEvent): void {
4966
event.preventDefault();
5067
setIsLoading(true);
5168

5269
axios
53-
.post<LoginResponse>(
54-
loginUrl,
55-
{ username, password },
56-
{
57-
withCredentials: true,
58-
headers: {
59-
'Content-Type': 'application/json',
60-
'X-CSRF-TOKEN': getCookie('csrf') || '',
61-
},
62-
},
63-
)
70+
.post<LoginResponse>(loginUrl, { username, password }, getAxiosConfig())
6471
.then(() => {
6572
window.sessionStorage.setItem('git.proxy.login', 'success');
6673
setMessage('Success!');
@@ -72,7 +79,7 @@ const Login: React.FC = () => {
7279
window.sessionStorage.setItem('git.proxy.login', 'success');
7380
setGitAccountError(true);
7481
} else if (error.response?.status === 403) {
75-
setMessage('You do not have the correct access permissions...');
82+
setMessage(processAuthError(error, false));
7683
} else {
7784
setMessage('You entered an invalid username or password...');
7885
}
@@ -113,52 +120,80 @@ const Login: React.FC = () => {
113120
/>
114121
</div>
115122
</CardHeader>
116-
<CardBody>
117-
<GridContainer>
118-
<GridItem xs={12} sm={12} md={12}>
119-
<FormControl fullWidth>
120-
<InputLabel htmlFor='username'>Username</InputLabel>
121-
<Input
122-
id='username'
123-
type='text'
124-
value={username}
125-
onChange={(e) => setUsername(e.target.value)}
126-
autoFocus
127-
data-test='username'
128-
/>
129-
</FormControl>
130-
</GridItem>
131-
</GridContainer>
132-
<GridContainer>
133-
<GridItem xs={12} sm={12} md={12}>
134-
<FormControl fullWidth>
135-
<InputLabel htmlFor='password'>Password</InputLabel>
136-
<Input
137-
id='password'
138-
type='password'
139-
value={password}
140-
onChange={(e) => setPassword(e.target.value)}
141-
data-test='password'
142-
/>
143-
</FormControl>
144-
</GridItem>
145-
</GridContainer>
146-
</CardBody>
147-
<CardFooter>
123+
{usernamePasswordMethod ? (
124+
<CardBody>
125+
<GridContainer>
126+
<GridItem xs={12} sm={12} md={12}>
127+
<FormLabel component='legend' style={{ fontSize: '1.2rem', marginTop: 10 }}>
128+
Login
129+
</FormLabel>
130+
<FormControl fullWidth>
131+
<InputLabel htmlFor='username'>Username</InputLabel>
132+
<Input
133+
id='username'
134+
type='text'
135+
value={username}
136+
onChange={(e) => setUsername(e.target.value)}
137+
autoFocus
138+
data-test='username'
139+
/>
140+
</FormControl>
141+
</GridItem>
142+
</GridContainer>
143+
<GridContainer>
144+
<GridItem xs={12} sm={12} md={12}>
145+
<FormControl fullWidth>
146+
<InputLabel htmlFor='password'>Password</InputLabel>
147+
<Input
148+
id='password'
149+
type='password'
150+
value={password}
151+
onChange={(e) => setPassword(e.target.value)}
152+
data-test='password'
153+
/>
154+
</FormControl>
155+
</GridItem>
156+
</GridContainer>
157+
</CardBody>
158+
) : (
159+
<CardBody>
160+
<FormLabel
161+
component='legend'
162+
style={{ fontSize: '1rem', marginTop: 10, marginBottom: 0 }}
163+
>
164+
Username/password authentication is not enabled at this time.
165+
</FormLabel>
166+
</CardBody>
167+
)}
168+
{/* Show login buttons if available (one on top of the other) */}
169+
<CardFooter style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
148170
{!isLoading ? (
149171
<>
150-
<Button
151-
color='success'
152-
block
153-
disabled={!validateForm()}
154-
type='submit'
155-
data-test='login'
156-
>
157-
Login
158-
</Button>
159-
<Button color='warning' block onClick={handleOIDCLogin} data-test='oidc-login'>
160-
Login with OIDC
161-
</Button>
172+
{usernamePasswordMethod && (
173+
<Button
174+
color='success'
175+
block
176+
disabled={!validateForm()}
177+
type='submit'
178+
data-test='login'
179+
>
180+
Login
181+
</Button>
182+
)}
183+
{authMethods.map((am) => (
184+
<Button
185+
color='success'
186+
block
187+
onClick={() => handleAuthMethodLogin(am)}
188+
data-test={`${am}-login`}
189+
key={am}
190+
>
191+
Login
192+
{authMethods.length > 1 || usernamePasswordMethod
193+
? ` with ${am.toUpperCase()}`
194+
: ''}
195+
</Button>
196+
))}
162197
</>
163198
) : (
164199
<div style={{ textAlign: 'center', width: '100%', opacity: 0.5, color: 'green' }}>
@@ -168,7 +203,12 @@ const Login: React.FC = () => {
168203
</CardFooter>
169204
</Card>
170205
<div style={{ textAlign: 'center', opacity: 0.9, fontSize: 12, marginTop: 20 }}>
171-
<Badge overlap='rectangular' color='error' badgeContent='NEW' />
206+
<Badge
207+
overlap='rectangular'
208+
color='error'
209+
badgeContent='NEW'
210+
style={{ marginRight: 20 }}
211+
/>
172212
<span style={{ paddingLeft: 20 }}>
173213
View our <a href='/dashboard/push'>open source activity feed</a> or{' '}
174214
<a href='/dashboard/repo'>scroll through projects</a> we contribute to

0 commit comments

Comments
 (0)