Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add keycloak - initial #87

Merged
merged 5 commits into from
Mar 13, 2022
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
8 changes: 8 additions & 0 deletions app/.env.local.sample
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
COOKIE_SECRET_CURRENT=
COOKIE_SECRET_PREVIOUS=

### BEGIN: Environment variables for keycloak
NEXTAUTH_SECRET="some random string for signing"
NEXTAUTH_URL="http://localhost:3000"
KEYCLOAK_ID='keycloak client id'
KEYCLOAK_SECRET='keycloak client secret'
KEYCLOAK_ISSUER='http://localhost:8080/realms/{Realm Name}'
### END: Environment variables for keycloak

# INAPPCHAT
# for development http://localhost:3000
NEXT_PUBLIC_ROCKET_CHAT_HOST=required
Expand Down
2 changes: 1 addition & 1 deletion app/components/auth/NoUserAvatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function NoUserAvatar({size,name}){
const char = name ? name.charAt(0) : null;
return (
<div
className="d-flex justify-content-center align-items-center"
className="d-flex justify-content-center align-items-center text-capitalize"
style={{
width: `${size}px`,
height: `${size}px`,
Expand Down
79 changes: 79 additions & 0 deletions app/components/auth/keycloak/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

### Keycloak configuration
1. Create an openid-connect client in Keycloak with "confidential" as the "Access Type". See [https://www.keycloak.org/docs/latest/server_admin/#_oidc_clients](https://www.keycloak.org/docs/latest/server_admin/#_oidc_clients).

2. To add profile picture attribute received from providers, You will have to create a mapper to set profile picture url into `picture` attribute.

### Set up
We have used `next-auth` npm package to configure keycloak authentication. Read more about it [here](https://next-auth.js.org/)

1. set up environment variables for firebase config in `.env.local`

```
### BEGIN: Environment variables for keycloak
NEXTAUTH_SECRET="some random string for signing"
NEXTAUTH_URL="http://localhost:3000"
KEYCLOAK_ID='keycloak client id'
KEYCLOAK_SECRET='keycloak client secret'
KEYCLOAK_ISSUER='http://localhost:8080/realms/{Realm Name}'
### END: Environment variables for keycloak
```

2. Wrap the app with `SessionProvider` in _app.js in the following way
```
import '/styles/globals.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import Layout from '../components/layout';
import SSRProvider from 'react-bootstrap/SSRProvider';
import {SessionProvider} from 'next-auth/react';

function MyApp({ Component, pageProps: {session, ...pageProps}}) {
return (
<SSRProvider>
<SessionProvider session={session}>
<Layout menu={pageProps}>
<Component {...pageProps} />
</Layout>
</SessionProvider>
</SSRProvider>
);
}

export default MyApp;

```
3. Add `KeycloakAuthMenuButton` in menubar.
```
...
import {KeycloakAuthMenuButton} from './auth/keycloak';
...

export default function Menubar(props) {
...

return (
<Container fluid className='border-bottom '>
<Navbar expand='lg' className=' bg-white mx-4 my-2'>
...
<div className="mx-1">
<KeycloakAuthMenuButton/>
</div>
</Navbar>
</Container>
);
}
```
4. Use `useSession()` webhook to get user data.
```
import { useSession } from "next-auth/react";
...
...
export default Component(){
const {data:session} = useSession();
if(!session)
return <div/>;
const user = session.user;
return <div>Hello ${user.name}</div>
}
```
5. Sign out using custom sign out function `signOutKC()`. This will sign out user from keycloak as well.
7 changes: 7 additions & 0 deletions app/components/auth/keycloak/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import keycloakAuthMenuButtonModule from "./ui/KeycloakAuthMenuButton";
import keycloakAuthUIModule from "./ui/KeycloakAuthUI";
import signOutKCModule from './lib/signOutKC';

export const KeycloakAuthMenuButton = keycloakAuthMenuButtonModule;
export const KeycloakAuthUI = keycloakAuthUIModule;
export const signOutKC = signOutKCModule;
30 changes: 30 additions & 0 deletions app/components/auth/keycloak/lib/signOutKC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { BroadcastChannel } from "next-auth/client/_utils"
import { getCsrfToken } from "next-auth/react";

const broadcast = new BroadcastChannel();

export default async function signOutKC({
callbackUrl = null
}){
const csrfToken = await getCsrfToken();
const fetchOptions = {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken,
callbackUrl: `/api/auth/signoutkc?token=${csrfToken}${callbackUrl?'&callbackUrl='+encodeURIComponent(callbackUrl):''}`,
json: true,
}),
}
const res = await fetch(`/api/auth/signout`, fetchOptions)
const data = await res.json();

broadcast.post({ event: "session", data: { trigger: "signout" } })

const url = data.url ?? callbackUrl
window.location.href = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.authDialogWrapper {
position: relative;
}
.authContainer {
display: block;
position: fixed;
right: 50%;
transform: translate(50%,0);
top: 62px;
width: 350px;
max-height: -webkit-calc(100vh - 62px - 100px);
max-height: calc(100vh - 62px - 100px);
overflow-y: auto;
overflow-x: hidden;
border-radius: 8px;
margin-left: 12px;
z-index: 991;
line-height: normal;
background: #fff;
border: 1px solid #ccc;
border-color: rgba(0,0,0,.2);
color: #000;
-webkit-box-shadow: 0 2px 10px rgb(0 0 0 / 20%);
box-shadow: 0 2px 10px rgb(0 0 0 / 20%);
-webkit-user-select: text;
user-select: text;
}
@media(min-width: 570px) {
.authContainer {
position: absolute;
right: 8px;
top: 62px;
width: 354px;
transform: translateX(0);
}
}
.avatar {
background: var(--bs-gray-300);
border-radius: 50%;
width: 42px;
height: 42px;
display: flex;
justify-content: center;
align-items: center;
}

.avatarButton {
background: none;
border: none;
}

.avatarButton:focus {
outline: none;
}
7 changes: 7 additions & 0 deletions app/components/auth/keycloak/styles/KeycloakAuthUI.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.authUIWrapper {
width: 100%;
max-width: 400px;
border: 1px solid #ddd;
box-shadow: 2px 2px 3px 3px #0000001D;
background: #FFF;
}
51 changes: 51 additions & 0 deletions app/components/auth/keycloak/ui/KeycloakAuthMenuButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useRef, useState } from "react";
import KeycloakAuthUI from "./KeycloakAuthUI";
import { NoUserAvatar } from "../../NoUserAvatar";
import styles from "../styles/KeycloakAuthMenuButton.module.css";
import { signIn, useSession } from "next-auth/react";

export default function KeycloakAuthMenuButton({}){
const {data: session} = useSession();
const user = session?.user;
const [isOpen,setOpen] = useState(false);
const dialogRef = useRef();
const onAvatarButtonClick = () => {
if(session){
setOpen(!isOpen);
} else {
signIn("keycloak",null,{prompt: "login"});
}
}
useEffect(()=>{
const clickListener = (e) => {
if(!e.target.closest('.'+styles.authDialogWrapper)){
setOpen(false);
}
}
document.body.addEventListener('click',clickListener);
return () => document.body.removeEventListener('click',clickListener);
},[dialogRef.current]);
return (
<div className={styles.authDialogWrapper} ref={dialogRef}>
<div className={styles.avatar}>
<button className={styles.avatarButton} onClick={onAvatarButtonClick}>
<span className="d-flex align-items-center">
{
user?.image ?
<img src={user.image}
alt={user.name}
className="rounded-circle"
height="42px"
width="42px" />
:
<NoUserAvatar name={user?.name} size="42" />
}
</span>
</button>
</div>
{ session && isOpen &&
<div className={styles.authContainer}><KeycloakAuthUI/></div>
}
</div>
)
}
50 changes: 50 additions & 0 deletions app/components/auth/keycloak/ui/KeycloakAuthUI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useSession } from "next-auth/react";
import { Button } from "react-bootstrap";
import { NoUserAvatar } from "../../NoUserAvatar";
import signOutKC from "../lib/signOutKC";

export default function KeycloakUserInfo(){
const {data:session} = useSession();
if(!session)
return <div/>;
const user = session.user;
return (
<>
<div className="d-flex flex-column align-items-center mt-4 mb-3 ml-3 mr-3 border-bottom">
<div className="mb-1">
{
user.image ?
<img src={user.image}
alt={user.name}
style={{
borderRadius: "50%"
}}
height="64px"
width="64px" />
:
<NoUserAvatar size="64" name={user.name}/>
}
</div>
<div className="font-weight-bold mb-1">
{user.name}
</div>
<div
className="mb-1"
style={{color: "var(--bs-gray-700)"}}>
{user.email}
</div>
<div
className="mb-1"
style={{color: "var(--bs-gray-700)"}}>
<a href="/api/auth/profilekc">Manage profile</a>
</div>
</div>
<div className="d-flex justify-content-center mb-4 mt-3 ml-3 mr-3">
<Button variant="secondary"
onClick={() => signOutKC({callbackUrl: window.location.href})}>
Sign Out
</Button>
</div>
</>
)
}
3 changes: 1 addition & 2 deletions app/components/layout.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import '../styles/Layout.module.css'
import { withFirebaseAuthUser } from './auth/firebase';
import Footer from './footer'
import Menubar from './menubar'

Expand All @@ -15,4 +14,4 @@ function Layout(props) {
)
}

export default withFirebaseAuthUser()(Layout);
export default Layout;
4 changes: 2 additions & 2 deletions app/components/menubar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Navbar, Nav, NavDropdown, Container } from 'react-bootstrap';
import styles from '../styles/Menubar.module.css';
import { FirebaseAuthMenuButton } from './auth/firebase';
import {KeycloakAuthMenuButton} from './auth/keycloak';
import BrandLogo from "./brandlogo";
import RocketChatLinkButton from './rocketchatlinkbutton';

Expand Down Expand Up @@ -71,7 +71,7 @@ export default function Menubar(props) {
</RocketChatLinkButton>
</Navbar.Collapse>
<div className="mx-1">
<FirebaseAuthMenuButton/>
<KeycloakAuthMenuButton/>
</div>
</Navbar>
</Container>
Expand Down
Loading