Secure React SPA using Keycloak with OpenID Connect in 2021
In the light of my previous post “Secure React SPA using Azure Endpoints with Authorization code flow” I realized that configuring multiple providers with you application needs lots of coding and configuration and specially when you want to stick with Authorization code flow for all the providers. While Azure, Google supports a request from cross origin requests in a way and on other hand Facebook strictly doesn’t support it. (So we needed to create a proxy server to modify CORS policy to get your request accepted).
1. Introduction
Thanks to WildFly developer community for developing a open source project Keycloak. “Keycloak is an open source software product to allow single sign-on with Identity and Access Management aimed at modern applications and services” as from Wikipedia. All you need to do is keep Keycloak application server running on a machine whether it is on same domain or cross domain doesn’t matter. In this post we are going to learn about running a Keycloak server and a React SPA will integrate with it.
2. Working Demo
3. Keycloak setup and configuration
I am using VirtualBox with Linux setup for running Keycloak. I am also keeping Keycloak behind a Nginx server for stuffs like reverse proxy, load balancing. For running Keycloak I am using docker image jboss/keycloak form Docker hub.
For http it uses port 8080,docker run --detach -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin docker.io/jboss/keycloakFor https it uses port 8443,docker run --detach -p 8443:8443 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin docker.io/jboss/keycloak
This will create an account for you as well with admin username and admin password, you can change the username and password from changing the value from parameter KEYCLOAK_USER=<> and KEYCLOAK_PASSWORD=<>.
Now in Nginx for I have done these configuration for reverse proxy,
location /auth/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
#proxy_redirect off;
add_header Pragma "no-cache";
add_header Cache-Control "no-cache";
proxy_pass https://localhost:8443/auth/;
sub_filter_once off;
}
Since I am using https so we need SSL certificates for it, for now I am using self signed certificate, you can configure it from here.
And that’s to run an admin account on Keycloak application server. Now we will do our provider configuration. Here are the steps for that,
- Create a realm by clicking on “Add realm” and then fill the name of realm.
- Once you have created the realm you can create a client for your realm from here,
- Fill the client ID, that will be further used by your React application. Keep the client protocol as openid-connect.
- After creating a client you need to fill specific details for that particular client as shown below. Focus on the highlighted fields. Access Type, this needs to be public since we are doing this configuration for a public client. Valid Redirect URIs, this is the uri in your application where you are expecting to receive tokens after doing successful authentication from Keycloak. Web Origins, mentioning urls at this field will enable you for CORS at Keycloak application server, hence you will not get any CORS related issue.
- Last but the most important configuration, on the same client configuration page below under Advance Settings you need to specify the method that has been used by you client to create the code challenge on code verifier. As per PKEC RFC this should be ‘sha256’, if client(React SPA) is capable of using it otherwise this can also be ‘plain’.
Congratulations !! you have successfully configured you Keycloak Application Server to properly work it like an Authentication Server.
4. Source Code and Configuration for React SPA
I have a code sample ready for this working demo as shown in above picture. Clone the repo and install the node_modules
git clone https://github.com/surya5954/LoginAs.git
cd LoginAs
npm install
Open .env file in you editor and update below mentioned field,
REACT_APP_KEYCLOAK_ID=<Client ID from Keycloak admin portal>
Now coming back to our application, let’s start to understand the logic behind this magic,
PKCE related configuration,
CODE_VERIFIER : As per PKCE RFC this need to be minimum 43 bit long URL-Safe random string using unreserved characters [A-Z] / [a-z] / [0–9] / “-” / “.” / “_” / “~”.
CODE_CHALLENGE_METHOD : As per PKEC RFC this should be ‘sha256’, if client is capable of using it otherwise this can also be ‘plain’.
CODE_CHALLENGE : In case of ‘sha256’ it should be Base64 URL-encoded SHA-256 hash of the code verifier.
In <repo path>/loginas/src/config/PCKEConfigs.js,
import crypto from 'crypto';// Some random 64 bit long string
export const CODE_VERIFIER = 'AdleUo9ZVcn0J7HkXOdzeqN6pWrW36K3JgVRwMW8BBQazEPV3kFnHyWIZi2jt9gA';export const CODE_CHALLENGE_METHOD = 'S256';
const base64URLEncode = (str) => {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
const getCodeChallange = (verifier) => {
return base64URLEncode(crypto.createHash('sha256')
.update(verifier).digest());
}
export const CODE_CHALLENGE = getCodeChallange(CODE_VERIFIER);
This piece of code generates the dynamic secret that we were taking about and helps your client application to be a rightful client in eyes of Keycloak Authentication server to obtain access_token.
Authorize Endpoint configuration,
Now here is how you construct your Authorization endpoint call with PKCE to get a code in response,
In <repo path>/loginas/src/container/AuthEndpointSetup/Keycloak.js,
const Keycloak = () => {
return queryString.stringifyUrl({
url: `http://<Keycloak server hostname>/auth/realms/todo/protocol/openid-connect/auth`,
query: {
client_id: KEYCLOAK_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: [
'openid',
'profile',
].join(" "),
state: JSON.stringify({ provider: 'Keycloak' }),
code_challenge: CODE_CHALLENGE,
code_challenge_method: CODE_CHALLENGE_METHOD
}
});
}
Fetching Code from URL,
Next we need to fetch to code details from URL to construct a call for token endpoint,
In <repo path>/loginas/src/container/Layout/Layout.js,
componentDidMount() {
let code_url = /((\?|\&)code\=)[^\&]+/.exec(this.props.location.search);
let state_provider = /((\?|\&)state\=)[^\&]+/.exec(this.props.location.search);
if (code_url != null) {
let code = decodeURIComponent(String(code_url[0]).replace(/(\?|\&)?code\=/, ''));
let provider_obj = String(decodeURIComponent(state_provider[0])).replace(/(\?|\&)?state\=/, '');
const provider = JSON.parse(provider_obj).provider;
this.setState({ enableProgressBar: true })
const token = window.localStorage.getItem('token');
if (token == null) {
this.getAccessTokenFromCode(provider, code);
} else {
this.props.history.push('/welcome/user');
}}
}
Token Endpoint configuration,
Since now we have got the code from URL, now need to construct to token endpoint URL which is a post call,
In <repo path>/loginas/src/container/TokenEndpointSetup/KeycloakToken.js,
const KeycloakToken = async (code) => {
let params = {
client_id: KEYCLOAK_ID,
code: code,
grant_type: 'authorization_code',
redirect_uri: REDIRECT_URI,
code_verifier: CODE_VERIFIER,
state: JSON.stringify({ provider: 'Keycloak' }),
}const post_data = queryString.stringify(params);
let parsedUrl = URL.parse(`http://<Keycloak server Hostname>/auth/realms/todo/protocol/openid-connect/token`, true);let realHeaders = {};
realHeaders['Host'] = parsedUrl.host;
realHeaders["Content-Length"] = post_data.length;
realHeaders["Content-Type"] = 'application/x-www-form-urlencoded';const options = {
host: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname,
method: "POST",
headers: realHeaders
};const payload = Object.assign({
body: post_data
}, options);
let response = await fetch(`http://<Keycloak server Hostname>/auth/realms/todo/protocol/openid-connect/token`, payload);let res = await response.json();
return res.access_token;}
Now in this post call we pass that 64 bit long random string as code_verifier along with code that we have fetched from URL.
Once you have got the access_token, you will also be like,
Fetching User details,
Now all you need to do is sit like boss and ask for user details, here are the code snippet that I have used to get name and email detail for user. Although the access_token in this case is a JWT token so you can get some limited details about user from that token as well, but I have called a userinfo API to get some user details,
In <repo path>/loginas/src/container/UserDetails/KeycloakUser.js,
const KeycloakUser = async (token) => {
let userProvider;
const res = await fetch(`http://<Keycloak server Hostname>/auth/realms/todo/protocol/openid-connect/userinfo`, {
method: 'get',
headers: {
Authorization: `Bearer ${token}`,
}
})
const res_obj = await res.json();
if (res_obj.sub) {
userProvider = {
name: res_obj.name,
email: res_obj.email,}
}
return userProvider;
}
Conclusion
And that’s is all you need to do successful authentication with Keycloak endpoints directly from browser, no back-end involved. I hope you have found this helpful. Thank you for reading.
Happy Hacking!
Also Read :
How to create a blog in PHP and MySQL database in 2020?
How to Fetch Data in React with Example in 2021 ?
A practical guide on the CSS position property for 2021 Web Designers
Comments
Post a Comment