Skip to content

Commit

Permalink
Merge branch 'keycloak-dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
joschrew committed Sep 29, 2023
2 parents 96a559f + 91c65be commit bab83a8
Show file tree
Hide file tree
Showing 15 changed files with 1,461 additions and 1,081 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"jquery": "^3.4.1",
"jquery.easing": "^1.4.1",
"moment": "^2.24.0",
"oidc-client": "^1.11.5",
"popper.js": "^1.15.0",
"postcss": "^8.4.13",
"postcss-loader": "^4",
Expand Down
28 changes: 28 additions & 0 deletions src/auth/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { keycloakAuthService } from "./authKeycloak";
import { customAuthService } from "./authCustom";
import axios from "../axios-config";

export let authService;

if (process.env.VUE_APP_USE_KEYCLOAK && process.env.VUE_APP_USE_KEYCLOAK.toUpperCase() == "TRUE") {
authService = keycloakAuthService;
} else {
authService = customAuthService;
}

function setBearerInterceptor () {
// Add the bearer token to all outgoing requests if available
axios.interceptors.request.use(async (config) => {
let accessToken;
try {
accessToken = await authService.getAccessToken()
if (accessToken != null) {
config.headers.common.Authorization = 'Bearer ' + accessToken
}
} catch (e) {
// pass: if AccessToken cannot be received do not use bearer header
}
return config
})
}
setBearerInterceptor();
76 changes: 76 additions & 0 deletions src/auth/authCustom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import axios from "../axios-config";

/**
* This app currently offers two ways of authentification: custom and keyloak. The custom login uses
* the backend for log-in. The keyloak service an external Keyloak server not managed by us.
*
* This service has functions which KeycloakAuthService has as well. This functions should be usable
* interchangeable depending on which auth-mechanism is used. All the functions of the
* KeycloakAuthService have to async. I read that it is possible to await a non async function, that
* way it's result is automatically wrapped into a promise. That's why not all functionse are not
* async as well.
*/
class CustomAuthService {

constructor() {
this._loggedIn = false;
this._loggedInListeners = [];
this.token = null;
this.username = null;
this.expiredTime = 0;
}

set loggedIn(value) {
this._loggedIn = value
this._loggedInListeners.forEach(fn => fn(value))
}

addLoggedInListener(fn) {
this._loggedInListeners.push(fn)
}

isUserLoggedIn() {
return this._loggedIn
}

async loginCustom(username, password) {
// Try to login
const formData = new FormData();
formData.append("username", username)
formData.append("password", password)

return axios
.post("/login", formData)
.then((response) => {
// Save the information
this.token = response.data.accessToken,
this.expiredTime = response.data.expiredTime,
this.username = username,
this._loggedIn = true;
});
}

logoutCustom() {
this.token = null;
this.username = null;
this.expiredTime = 0;
this._loggedIn = false;
}

getAccessToken() {
if (!this.token || Date.now() >= this.expiredTime) {
return null
} else {
return this.token
}
}

getUsername() {
return this.username;
}

isKeycloak() {
return false;
}
}
export const customAuthService = new CustomAuthService()
138 changes: 138 additions & 0 deletions src/auth/authKeycloak.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { UserManager, WebStorageStateStore } from 'oidc-client'

const settings = {
authority: process.env.VUE_APP_KEYCLOAK_AUTHORITY,
client_id: process.env.VUE_APP_KEYCLOAK_CLIENT_ID,
redirect_uri: process.env.VUE_APP_KEYCLOAK_REDIRECT_URI,
post_logout_redirect_uri: process.env.VUE_APP_KEYCLOAK_LOGOUT_REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email offline_access',
automaticSilentRenew: true,
}

settings.userStore = new WebStorageStateStore({ store: window.sessionStorage })
const userManager = new UserManager(settings)

/**
* Class to encapsulate all authentication related logic.
*/
class KeycloakAuthService {

constructor() {
this._loggedIn = false;
this._loggedInListeners = [];
}

set loggedIn(value) {
this._loggedIn = value
this._loggedInListeners.forEach(fn => fn(value))
}

addLoggedInListener(fn) {
this._loggedInListeners.push(fn)
}

/**
* Checks whether or not a user is currently logged in.
*
* Returns a promise which will be resolved to true/false or be rejected with an error.
*/
async isUserLoggedIn () {
try {
let user = await userManager.getUser();
this.loggedIn = (user !== null)
} catch (error) {
this.loggedIn = false
}
return this._loggedIn
}

/**
* Initate the login process.
*/
loginKeycloak () {
userManager.signinRedirect()
}

logoutKeycloak () {
userManager.signoutRedirect()
}

/**
* Handles the redirect from the OAuth server after a user logged in.
*/
handleLoginRedirect () {
// Returns a promise
try {
return userManager.signinRedirectCallback()
} catch (error) {
throw error
}
}

/**
* Handles the redirect from the OAuth server after a user logged out.
*/
handleLogoutRedirect () {
return userManager.signoutRedirectCallback()
}

/**
* Get the access token.
*
* Can be used to make requests to the backend.
*/
async getAccessToken () {
return new Promise((resolve, reject) => {
userManager.getUser()
.then(user => {
if (user != null) {
resolve(user.access_token)
} else {
reject("User not logged in. Cannot read Access Token")
}
})
.catch(error => {
reject(error);
})
})
}

/**
* Get the profile data for the currently authenticated user.
*
* Returns an empty object if no user is logged in.
*/
async getProfile () {
return new Promise((resolve, reject) => {
userManager.getUser()
.then(user => {
if (user === null) {
resolve(null)
} else {
resolve(user.profile)
}
})
.catch(error => reject(error))
})
}

isKeycloak() {
return true;
}

async getUsername() {
let profile = await this.getProfile();
if (profile) {
return profile["sub"];
}
//return this.username;
return null;
}

}

/**
* Create and expose an instance of the auth service.
*/
export const keycloakAuthService = new KeycloakAuthService()
6 changes: 4 additions & 2 deletions src/components/dashview/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
<script>
import axios from "../../axios-config";
import moment from "moment";
import { authService } from "../../auth/auth"
export default {
data() {
Expand Down Expand Up @@ -143,16 +144,17 @@ export default {
},
},
methods: {
fetchData(page, limit) {
async fetchData(page, limit) {
// Init state
this.loading = true;
this.error = false;
// Fetch data
let username = await authService.getUsername()
axios
.get("/admin/import-status", {
params: {
username: this.$store.getters.username,
username: username,
page,
limit,
},
Expand Down
7 changes: 2 additions & 5 deletions src/components/login/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

<script>
import Modal from "../modal/Modal.vue";
import { authService } from "../../auth/auth"
export default {
components: { Modal },
props: {
Expand Down Expand Up @@ -80,11 +81,7 @@ export default {
username: this.username,
password: this.password,
};
this.$store
.dispatch("login", {
username: formData.username,
password: formData.password,
})
authService.loginCustom(formData.username, formData.password)
.then((result) => {
// Closing modal
this.onClose();
Expand Down
83 changes: 83 additions & 0 deletions src/components/login/LoginBtnCustom.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<template>

<div>
<div v-if="!isUserLoggedIn">
<button
type="button"
class="
bg-sky-600
font-medium
px-4
py-2
shadow-sm
rounded-md
text-white
hover:bg-sky-700
"
id="user-menu-button"
aria-expanded="false"
aria-haspopup="true"
v-on:click.stop
@click="openLoginModal"
>
Sign In
</button>
<LoginModal :open="isOpen" :onClose="onClose" />
</div>
<div v-else>
<button
type="button"
class="
bg-sky-600
font-medium
px-4
py-2
shadow-sm
rounded-md
text-white
hover:bg-sky-700
"
id="user-menu-button"
aria-expanded="false"
aria-haspopup="true"
@click="logout"
>
Log out
</button>
</div>
</div>
</template>
<script>
import LoginModal from '../../components/login/Login.vue'
export default {
components: { LoginModal },
data() {
return {
isOpen: false,
isUserLoggedIn: false,
};
},
methods: {
onClose() {
this.isOpen = false;
},
openLoginModal() {
this.isOpen = true;
},
logout() {
this.authService.logoutCustom()
this.isUserLoggedIn = false
this.$router.push("/")
}
},
props: [
"authService"
],
updated() {
this.authService.addLoggedInListener((newVal) => {
this.isUserLoggedIn = newVal
})
this.isUserLoggedIn = this.authService.isUserLoggedIn();
}
}
</script>
Loading

0 comments on commit bab83a8

Please sign in to comment.