Simple user/password authentication in your web application with the Meecrogate OAuth2 server
Context
This blog post describes a front end implementation of the password flox through the meecrogate oauth2 server. We consider that the backend is read and that the oauth2 server is available. We will be focusing on how to implement the front end application with React and material to make it a bit more fancy. The first step is to register our client on the Oauth2 server with a password grant type. The Oauth2 server offers few flow options:
-
authorization code
-
PKCE
-
client credentials
-
password
-
refresh token
You can find more information about those different flows in here.
For this example we will register our client with 'Password' type, so from the front end we will have the following steps to manage:
-
go to the login page of the react application
-
login
-
retrieve the access_token
Typescript Example
This example has been implemented with React so it is in typescript and can be adapted for angular or other frameworks.
login component
We will not describe the whole react application but the login component will be enough.
import React, {useReducer, useEffect} from 'react';
import {createStyles, makeStyles, Theme} from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import CardHeader from '@material-ui/core/CardHeader';
import Button from '@material-ui/core/Button';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
container: {
display: 'flex',
flexWrap: 'wrap',
width: 400,
margin: `${theme.spacing(0)} auto`
},
loginBtn: {
marginTop: theme.spacing(2),
flexGrow: 1
},
header: {
textAlign: 'center',
background: '#212121',
color: '#fff'
},
card: {
marginTop: theme.spacing(10)
}
})
);
//state type
type State = {
username: string
password: string
isButtonDisabled: boolean
helperText: string
isError: boolean
};
const initialState: State = {
username: '',
password: '',
isButtonDisabled: true,
helperText: '',
isError: false
};
type Action = { type: 'setUsername', payload: string }
| { type: 'setPassword', payload: string }
| { type: 'setIsButtonDisabled', payload: boolean }
| { type: 'loginSuccess', payload: string }
| { type: 'loginFailed', payload: string }
| { type: 'setIsError', payload: boolean };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'setUsername':
return {
...state,
username: action.payload
};
case 'setPassword':
return {
...state,
password: action.payload
};
case 'setIsButtonDisabled':
return {
...state,
isButtonDisabled: action.payload
};
case 'loginSuccess':
return {
...state,
helperText: action.payload,
isError: false
};
case 'loginFailed':
return {
...state,
helperText: action.payload,
isError: true
};
case 'setIsError':
return {
...state,
isError: action.payload
};
}
}
const Login = () => {
const classes = useStyles();
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
if (state.username.trim() && state.password.trim()) {
dispatch({
type: 'setIsButtonDisabled',
payload: false
});
} else {
dispatch({
type: 'setIsButtonDisabled',
payload: true
});
}
}, [state.username, state.password]);
const handleLogin = () => {
const recipeUrl = 'http://localhost:8080/oauth2/token?grant_type=password&username=' + state.username + '&password=' + state.password + '&client_id=<your_client_id>&client_secret=<your_client_secret>';
const requestMetadata = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
fetch(recipeUrl, requestMetadata)
.then(res => res.json())
.then(token => {
if(token.access_token){
dispatch({
type: 'loginSuccess',
payload: 'Login Successfully'
});
}else{
dispatch({
type: 'loginFailed',
payload: token.error_description
});
}
});
};
const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.keyCode === 13 || event.which === 13) {
state.isButtonDisabled || handleLogin();
}
};
const handleUsernameChange: React.ChangeEventHandler<HTMLInputElement> =
(event) => {
dispatch({
type: 'setUsername',
payload: event.target.value
});
};
const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> =
(event) => {
dispatch({
type: 'setPassword',
payload: event.target.value
});
}
return (
<form className={classes.container} noValidate autoComplete="off">
<Card className={classes.card}>
<CardHeader className={classes.header} title="Login App"/>
<CardContent>
<div>
<TextField
error={state.isError}
fullWidth
id="username"
type="email"
label="Username"
placeholder="Username"
margin="normal"
onChange={handleUsernameChange}
onKeyPress={handleKeyPress}
/>
<TextField
error={state.isError}
fullWidth
id="password"
type="password"
label="Password"
placeholder="Password"
margin="normal"
helperText={state.helperText}
onChange={handlePasswordChange}
onKeyPress={handleKeyPress}
/>
</div>
</CardContent>
<CardActions>
<Button
variant="contained"
size="large"
color="secondary"
className={classes.loginBtn}
onClick={handleLogin}
disabled={state.isButtonDisabled}>
Login
</Button>
</CardActions>
</Card>
</form>
);
}
export default Login;
The code above is the whole component code. We will focus on the call to the oauth2 server in the next section.
retrieve the token
On our react application we have a method that will call the oauth2 server with the username/password provided by the user. The response will either contain the access token or return the error message.
const handleLogin = () => {
const recipeUrl = 'http://localhost:8080/oauth2/token?grant_type=password&username=' + state.username + '&password=' + state.password + '&client_id=<your_client_id>&client_secret=<your_client_secret>';
const requestMetadata = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
fetch(recipeUrl, requestMetadata)
.then(res => res.json())
.then(token => {
if(token.access_token){
dispatch({
type: 'loginSuccess',
payload: 'Login Successfully'
});
}else{
dispatch({
type: 'loginFailed',
payload: token.error_description
});
}
});
};
if the response contains the access_token we can forward the user to our application and use this token to contact protected apis and display the information. If the response does not contain the token but does contain an 'error_description' field we can then display this information to tell the user what problem he is facing.
Conclusion
In few steps we have been able to implement a simple password login flow for our web application with the Meecrogate Oauth2 Server. In this example we have been focusing on the simple password flow but you can check the other blog posts about more complex flows.