Controllers

This commit is contained in:
Karolis Kundrotas
2021-10-29 09:17:17 +03:00
parent aff6f8df82
commit ba413d4330
22 changed files with 675 additions and 292 deletions

View File

@@ -4,6 +4,22 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@azure/msal-browser": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.18.0.tgz",
"integrity": "sha512-YiWsimjsjjVu56+zOUDB1U3BCD9YNPIEZmw5iHzMk14aanqxWIvJlo+Ewo5/3FqxILgBOFWliH2hZCQPRIqKSg==",
"requires": {
"@azure/msal-common": "^5.0.1"
}
},
"@azure/msal-common": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-5.0.1.tgz",
"integrity": "sha512-CmPR3XM9+CGUu7V/+bAwDxyN6XqWJJhVLmv7utT3sbgay4l5roVXsD1t4wURTs8PwzxmmnJOrhvvGhoDxUW69g==",
"requires": {
"debug": "^4.1.1"
}
},
"@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
@@ -4289,7 +4305,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -7626,8 +7641,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multicast-dns": {
"version": "6.2.3",

View File

@@ -8,6 +8,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@azure/msal-browser": "^2.18.0",
"@popperjs/core": "^2.10.1",
"axios": "^0.20.0-0",
"bootstrap": "^5.1.1",

View File

@@ -19,15 +19,6 @@
</span>
</div>
</div>
<div v-if="$store.getters['auth/isExpiringSoon']" class="container">
<div class="alert alert-warning">
<h4 class="alert-heading">Greitai baisis jūsų sesija</h4>
<span>
Po {{ expiresIn }} baigsis jūsų sesija.
<a :href="$store.getters['auth/loginUrl']">Pratęsti sesija.</a>
</span>
</div>
</div>
<router-view />
</template>
@@ -46,7 +37,7 @@ export default {
}
},
created() {
this.$store.dispatch('auth/initialize')
this.$store.dispatch('msalAuth/initialize')
},
computed: {
isLocal() {
@@ -58,26 +49,6 @@ export default {
return location.protocol !== 'https:'
},
},
methods: {
updateExpiry() {
const totalSeconds = Math.floor(
(this.$store.getters['auth/expires'] - new Date()) / 1000
)
const seconds = totalSeconds % 60
const minutes = Math.floor(totalSeconds / 60)
if (minutes) {
this.expiresIn = `${minutes} min. ir ${seconds} sek.`
} else {
this.expiresIn = `${seconds} sek.`
}
},
},
mounted() {
this.interval = setInterval(this.updateExpiry, 1000)
},
beforeUnmount() {
clearInterval(this.interval)
},
}
</script>

View File

@@ -27,18 +27,19 @@
>Pagrindinis</router-link
>
</li>
<li class="nav-item">
<!-- <li class="nav-item">
<a href="/swagger" class="nav-link">Swagger UI</a>
</li>
</li> -->
</ul>
<div class="navbar-nav">
<span v-if="$store.getters['auth/isValid']" class="navbar-text"
>Prisijungta kaip {{ $store.getters['auth/email'] }}</span
<span v-if="$store.state.msalAuth.isLoggedIn" class="navbar-text"
>Prisijungta kaip {{ $store.state.msalAuth.displayName }}
<a href="#" @click="LogoutMsal" class="nav-link"
>Atsijungti</a
></span
>
<div v-else class="nav-item">
<a :href="$store.getters['auth/loginUrl']" class="nav-link"
>Prisijungti</a
>
<a href="#" @click="LoginMsal" class="nav-link">Prisijungti</a>
</div>
</div>
</div>
@@ -48,6 +49,8 @@
</template>
<script>
import { LoginMsal, LogoutMsal } from '@/msal'
export default {
name: 'NavMenu',
data() {
@@ -56,6 +59,8 @@ export default {
}
},
methods: {
LoginMsal,
LogoutMsal,
collapse() {
this.isExpanded = false
},

View File

@@ -4,6 +4,7 @@ import router from './router'
import store from './store'
import './assets/main.scss'
import 'bootstrap'
import './msal'
const app = createApp(App)

View File

@@ -0,0 +1,183 @@
import * as msal from '@azure/msal-browser'
import Cookies from 'cookies-js'
import axios from 'axios'
const ClientIdCookieName = 'ktusakacas'
const AuthorityCookieName = 'ktusakeksas'
const TenantCookieName = 'ktusalaimis'
const RequestedScopes = ['openid', 'email', 'profile']
const msalState = {
msal: null,
clientId: null, // 5931fda0-e9e0-4754-80c2-18bcb9d9561a
authority: null, // https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0
tenant: null, // 3415f2f7-f5a8-4092-b52a-003aaf844853,
stateChangeCallbacks: [],
isLoggedIn: false,
accessToken: null,
idToken: null,
email: null,
displayName: null,
debugFullTokenResponse: null,
}
async function initializeMSAL() {
if (msalState.msal != null) {
throw new Error('MSAL was attempted to initialize second time')
}
await __loadAuthParameters()
const msalConfig = {
auth: {
clientId: msalState.clientId,
authority: `https://login.microsoftonline.com/${msalState.tenant}`,
redirectUri: window.location.protocol + '//' + window.location.host + '/',
},
}
msalState.msal = new msal.PublicClientApplication(msalConfig)
msalState.msal.handleRedirectPromise().then(__handleResponse)
window.msalState = msalState
}
export function WatchMsalState(callback) {
msalState.stateChangeCallbacks.push(callback)
callback()
}
export function GetMsalState() {
return {
accessToken: msalState.accessToken,
idToken: msalState.idToken,
isLoggedIn: msalState.isLoggedIn,
debugFullTokenResponse: msalState.debugFullTokenResponse,
debugAccountInfo: msalState.debugAccountInfo,
email: msalState.email,
displayName: msalState.displayName,
}
}
export function LoginMsal() {
msalState.msal.loginRedirect({
scopes: RequestedScopes,
})
}
export function LogoutMsal() {
msalState.msal.logout()
}
async function __handleResponse(response) {
if (response !== null) {
if (__isAccountAceptable(response.account)) {
msalState.msal.setActiveAccount(response)
msalState.debugFullTokenResponse = response
__responseObjectToMsalState()
}
} else {
msalState.msal
.getAllAccounts()
.filter(__isAccountAceptable)
.forEach(account => {
msalState.msal.setActiveAccount(account)
})
const account = msalState.msal.getActiveAccount()
if (account != null) {
msalState.debugFullTokenResponse = await msalState.msal
.acquireTokenSilent({ scopes: RequestedScopes })
.catch(error => {
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return msalState.msal.acquireTokenRedirect({
scopes: RequestedScopes,
})
}
})
__responseObjectToMsalState()
}
}
__stateChanged()
}
function __responseObjectToMsalState() {
msalState.isLoggedIn = true
msalState.accessToken = msalState.debugFullTokenResponse.accessToken
msalState.idToken = msalState.debugFullTokenResponse.idToken
msalState.email = msalState.debugFullTokenResponse.idTokenClaims.email
msalState.displayName = msalState.debugFullTokenResponse.idTokenClaims.name
}
function __isAccountAceptable(account) {
if (account.tenantId != msalState.tenant) return false
return true
}
function __stateChanged() {
msalState.stateChangeCallbacks.forEach(cb => cb())
}
function __isLocalStorageAvailable() {
try {
localStorage.setItem('__lsTest', 'true')
const result = localStorage.getItem('__lsTest')
localStorage.removeItem('__lsTest')
return result == 'true'
} catch (e) {
return false
}
}
async function __loadAuthParameters() {
if (__isLocalStorageAvailable()) {
await __loadAuthParametersLocalStorage()
} else {
await __loadAuthParametersCookies()
}
}
async function __loadAuthParametersLocalStorage() {
const clientId = localStorage.getItem(ClientIdCookieName)
const authority = localStorage.getItem(AuthorityCookieName)
const tenant = localStorage.getItem(TenantCookieName)
if (clientId == null || authority == null || tenant == null) {
await __fetchAuthParameters()
localStorage.setItem(ClientIdCookieName, msalState.clientId)
localStorage.setItem(AuthorityCookieName, msalState.authority)
localStorage.setItem(TenantCookieName, msalState.tenant)
} else {
msalState.clientId = clientId
msalState.authority = authority
msalState.tenant = tenant
}
}
async function __loadAuthParametersCookies() {
const clientId = Cookies.get(ClientIdCookieName)
const authority = Cookies.get(AuthorityCookieName)
const tenant = Cookies.get(TenantCookieName)
if (clientId == null || authority == null || tenant == null) {
await __fetchAuthParameters()
Cookies.set(ClientIdCookieName, msalState.clientId)
Cookies.set(AuthorityCookieName, msalState.authority)
Cookies.set(TenantCookieName, msalState.tenant)
} else {
msalState.clientId = clientId
msalState.authority = authority
msalState.tenant = tenant
}
}
async function __fetchAuthParameters() {
var response = await axios.get('/api/AuthMetadata')
msalState.clientId = response.data.clientId
msalState.authority = response.data.authority
msalState.tenant = response.data.tenant
}
initializeMSAL()

View File

@@ -2,39 +2,34 @@
<div class="container">
<h1>KTU SA Problemų sprendimo sistema</h1>
<template v-if="$store.getters['auth/isValid']">
<template v-if="$store.state.msalAuth.isLoggedIn">
<div class="alert alert-success">
<h4 class="alert-heading">Tu esi prisijungęs</h4>
<span>
Kliento aplikacija turi tavo saugos raktą. Aplikacija žino, kad tavo
el. paštas yra: <b>{{ $store.getters['auth/email'] }}</b>
el. paštas yra: <b>{{ $store.state.msalAuth.email }}</b>
</span>
</div>
<h2>Visi laukai gaunami Azure Active Directory</h2>
<table class="table">
<thead>
<tr>
<th scope="col">Pavadinimas</th>
<th scope="col">Reikšmė</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in authDataTable" :key="key">
<td>{{ key }}</td>
<td>
<pre>{{ value }}</pre>
</td>
</tr>
</tbody>
</table>
<h3>Techninė duomenų reprezentacija</h3>
<h3>Priegos raktas (Access token)</h3>
<a
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens"
>Dokumentacija</a
>
<pre>{{ $store.state.msalAuth.accessToken }}</pre>
<h3>Indentifikacijos raktas (ID Token)</h3>
<a
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens"
>Dokumentacija apie laukų reikšmes</a
>Dokumentacija</a
>
<pre>{{ $store.state.auth.tokenData }}</pre>
<h3>Saugos raktas.</h3>
<pre>{{ $store.state.auth.token }}</pre>
<pre>{{ $store.state.msalAuth.idToken }}</pre>
<h4>Duomenys gaunami Indentifikacijos rakto (ID Token)</h4>
<pre>{{
$store.state.msalAuth.debugFullTokenResponse.idTokenClaims
}}</pre>
<h3>Autorizuotos sritys</h3>
<pre>{{ $store.state.msalAuth.debugFullTokenResponse.scopes }}</pre>
<h3>Serverio tokeno patikrinimas</h3>
<button type="button" class="btn btn-primary" @click="serverVerify">
Patikrinti
@@ -54,46 +49,20 @@
<script>
import axios from 'axios'
const names = {
aud: 'AppId (Audience)',
iss: 'Išdavėjas',
iat: 'Išdavimo momentas',
nbf: 'Negalioja anksčiau nei',
exp: 'Galiojimo pabaiga',
email: 'El. paštas',
nonce: 'Aplikacijos sugeneruota nepasikartojanti reikšmė',
sub: 'Subjektas (Vartotojo Id)',
tid: 'Tenanto Identifikatorius',
ver: 'OAuth versija',
}
function lookupName(key) {
if (names[key]) return names[key]
return key
}
export default {
data() {
return {
verificationResult: null,
}
},
computed: {
authDataTable() {
return Object.fromEntries(
Object.entries(this.$store.state.auth.tokenData).map(([key, value]) => [
lookupName(key),
value,
])
)
},
},
methods: {
serverVerify() {
this.verificationResult = null
axios
.get('/api/AuthMetadata/authed', {
headers: { Authorization: `Bearer ${this.$store.state.auth.token}` },
headers: {
Authorization: `Bearer ${this.$store.state.msalAuth.idToken}`,
},
})
.then(response => {
this.verificationResult = response.data

View File

@@ -1,25 +0,0 @@
<template>
<div class="container">
<h1>Palaukite kol nustatysime jūsų tapatybe...</h1>
</div>
</template>
<script>
const tokenRegex = /id_token=(.*\..*\..*)&/
export default {
name: 'OIDC',
created() {
if (this.openIdToken)
this.$store.dispatch('auth/setToken', this.openIdToken)
this.$router.push({ name: 'Home' })
},
computed: {
openIdToken() {
const matches = this.$route.hash.match(tokenRegex)
if (!matches) return null
return matches[1]
},
},
}
</script>

View File

@@ -1,6 +1,5 @@
import { createWebHistory, createRouter } from 'vue-router'
import Home from '@/pages/Home.vue'
import OidcEndpoint from '@/pages/OidcEndpoint.vue'
const routes = [
{
@@ -8,11 +7,6 @@ const routes = [
name: 'Home',
component: Home,
},
{
path: '/oidc',
name: 'OpenID connect endpoint',
component: OidcEndpoint,
},
]
const router = createRouter({

View File

@@ -1,11 +1,11 @@
import { createStore, createLogger } from 'vuex'
import auth from './modules/auth'
import msalAuth from './modules/msalAuth'
const debug = process.env.NODE_ENV !== 'production'
export default createStore({
modules: {
auth,
msalAuth,
},
strict: debug,
plugins: debug ? [createLogger()] : [],

View File

@@ -1,153 +0,0 @@
import Cookies from 'cookies-js'
import jwt_decode from 'jwt-decode'
import axios from 'axios'
const TokenCookieName = 'ktusaktutoken'
const ClientIdCookieName = 'ktusakacas'
const AuthorityCookieName = 'ktusakeksas'
const TenantCookieName = 'ktusalaimis'
const NonceCookieName = 'ktusakumpikas'
const Scope = 'openid email'
// initial state
const state = () => ({
token: null,
tokenData: null,
clientId: null, // 5931fda0-e9e0-4754-80c2-18bcb9d9561a
authority: null, // https://login.microsoftonline.com/3415f2f7-f5a8-4092-b52a-003aaf844853/v2.0
tenant: null, // 3415f2f7-f5a8-4092-b52a-003aaf844853
})
const callbackUrl =
window.location.protocol + '//' + window.location.host + '/oidc'
// getters
const getters = {
isReady(state) {
if (
state.clientId == null ||
state.authority == null ||
state.tenant == null
)
return false
return true
},
isValid(state, getters) {
if (!getters.isReady) return false
if (state.token == null || state.tokenData == null) return false
const d = state.tokenData
if (d.nonce !== state.nonce) return false
if (d.iss !== state.authority) return false
if (d.aud !== state.clientId) return false
const now = new Date()
const exp = new Date(d.exp * 1000)
if (now > exp) return false
return true
},
email(state, getters) {
if (!getters.isValid) return null
return state.tokenData.email
},
userId(state, getters) {
if (!getters.isValid) return null
return state.tokenData.sub
},
isExpiringSoon(state, getters) {
if (!getters.isValid) return false
return true
},
expires(state, getters) {
if (!getters.isValid) return 0
return new Date(state.tokenData.exp * 1000)
},
loginUrl(state, getters) {
if (!getters.isReady) return null
return `https://login.microsoftonline.com/${
state.tenant
}/oauth2/v2.0/authorize?client_id=${
state.clientId
}&redirect_uri=${encodeURIComponent(
callbackUrl
)}&response_type=id_token&scope=${Scope}&nonce=${state.nonce}`
},
}
// actions
const actions = {
async initialize({ commit }) {
const token = Cookies.get(TokenCookieName)
const primaryClientId = Cookies.get(ClientIdCookieName)
const primaryAuthority = Cookies.get(AuthorityCookieName)
const primaryTenant = Cookies.get(TenantCookieName)
const nonce = Cookies.get(NonceCookieName)
if (!nonce) {
const newNonce =
Date.now().toString(36) +
Math.random()
.toString(36)
.substring(2)
Cookies.set(NonceCookieName, newNonce)
commit('setNonce', newNonce)
} else {
commit('setNonce', nonce)
}
commit('setToken', token)
commit('computeTokenVars')
commit('setMetadata', [primaryClientId, primaryAuthority, primaryTenant])
axios
.get('/api/AuthMetadata')
.then(response => {
Cookies.set(ClientIdCookieName, response.data.clientId)
Cookies.set(AuthorityCookieName, response.data.authority)
Cookies.set(TenantCookieName, response.data.tenant)
commit('setMetadata', [
response.data.clientId,
response.data.authority,
response.data.tenant,
])
})
.catch(error => {
console.error(error)
})
},
async setToken({ commit }, token) {
Cookies.set(TokenCookieName, token)
commit('setToken', token)
commit('computeTokenVars')
},
}
// mutations
const mutations = {
setToken(state, token) {
state.token = token
},
setNonce(state, nonce) {
state.nonce = nonce
},
computeTokenVars(state) {
if (state.token == null) return
try {
state.tokenData = jwt_decode(state.token)
} catch {
console.log('Token was invalid.')
state.tokenData = null
state.token = null
}
},
setMetadata(state, [clientId, authority, tenant]) {
state.clientId = clientId
state.authority = authority
state.tenant = tenant
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}

View File

@@ -0,0 +1,46 @@
import { WatchMsalState, GetMsalState } from '@/msal'
// initial state
const state = () => ({
isLoggedIn: false,
accessToken: null,
idToken: null,
email: null,
displayName: null,
debugFullTokenResponse: null,
debugAccountInfo: null,
})
// getters
const getters = {}
// actions
const actions = {
initialize({ commit }) {
WatchMsalState(() => {
commit('setState', GetMsalState())
})
},
}
// mutations
const mutations = {
setState(state, msalState) {
state.isLoggedIn = msalState.isLoggedIn
state.accessToken = msalState.accessToken
state.idToken = msalState.idToken
state.debugFullTokenResponse = msalState.debugFullTokenResponse
state.debugAccountInfo = msalState.debugAccountInfo
state.email = msalState.email
state.displayName = msalState.displayName
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}