Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e1ddb95
Inital passportjs integration
lukeIam Mar 24, 2023
be53b31
Merge remote-tracking branch 'origin/master' into auth_passportjs
lukeIam Mar 24, 2023
08676a6
Fix: small problem with this context in Auth.js
lukeIam Mar 24, 2023
62b0940
Added passport-openidconnect implementation
lukeIam Apr 14, 2023
812395b
Merge remote-tracking branch 'origin/master' into auth_passportjs
lukeIam Apr 14, 2023
7010a13
Fixes for passport local and allow empty password
advplyr Apr 16, 2023
8d00647
Merge branch 'master' into auth_passportjs
advplyr Apr 16, 2023
8b68543
Merge
advplyr Apr 29, 2023
4359ca2
Fix XAccel issue
advplyr Apr 29, 2023
95e6fef
Merge remote-tracking branch 'origin/master' into auth_passportjs
lukeIam May 27, 2023
dd9a385
Merge remote-tracking branch 'origin/master' into auth_passportjs
lukeIam Aug 12, 2023
f0f03ef
Merge remote-tracking branch 'origin/master' into auth_passportjs
lukeIam Sep 10, 2023
405c954
Updated + first rough implementation
lukeIam Sep 13, 2023
af4c350
Use a short-time cookie to remember where to callback to
lukeIam Sep 14, 2023
226a774
Merge remote-tracking branch 'origin/master' into auth_passportjs
lukeIam Sep 16, 2023
6aaf3f0
Fix bug with undefined property
lukeIam Sep 16, 2023
91d8451
Remove log messages
lukeIam Sep 16, 2023
7af3033
Fix: ci error - no token sercret
lukeIam Sep 16, 2023
763c0f4
add missing await
lukeIam Sep 16, 2023
942aa93
Fix: local login not possible
lukeIam Sep 16, 2023
0a6cd89
Allow rest mode login (?isRest=true)
lukeIam Sep 17, 2023
51b0750
Merge remote-tracking branch 'origin/master' into auth_passportjs
lukeIam Sep 20, 2023
2c90bba
small refactorings
lukeIam Sep 20, 2023
f6113e8
cookie lifetime
lukeIam Sep 20, 2023
45cf00b
fix openid + jwt auth
lukeIam Sep 20, 2023
2c25f64
Add /auth_methods route
lukeIam Sep 20, 2023
0e75c80
prepare show/hide of login buttons
lukeIam Sep 20, 2023
7a13188
show/hide of login buttons
lukeIam Sep 23, 2023
f42ab45
Update passwordless root user check to user user.type instead of user.id
advplyr Sep 23, 2023
9922294
Fix setting tokenSecret on init
advplyr Sep 23, 2023
f6de373
Update /status endpoint to return available auth methods, fix socket …
advplyr Sep 24, 2023
7ba10db
Update login button openid and google urls
advplyr Sep 24, 2023
e282142
Add authentication page in config, add /auth-settings GET endpoint, r…
advplyr Sep 24, 2023
0d5a30b
Update JWT auth extractors, add state in openid redirect, add back co…
advplyr Sep 25, 2023
2662e8f
Merge branch 'master' into auth_passportjs
advplyr Oct 2, 2023
ab14b56
Merge master
advplyr Nov 1, 2023
828b96b
Add server settings for changing openid button text and auto launchin…
advplyr Nov 2, 2023
f15ed08
Merge branch 'master' into auth_passportjs
advplyr Nov 2, 2023
cfe0c2a
Merge branch 'master' into auth_passportjs
advplyr Nov 3, 2023
840811b
Replace passport openidconnect plugin with openid-client, add JWKS an…
advplyr Nov 4, 2023
1e5d6a5
Added swedish translation of strings
ScuttleSE Nov 5, 2023
61e05e9
Add Swedish language option
advplyr Nov 5, 2023
309ef80
Update /auth/openid endpoint to work with PKCE from mobile
advplyr Nov 5, 2023
c17540e
Add app and serverVersion properties to response from /status
advplyr Nov 5, 2023
f840aa8
Add button to populate openid URLs using the issuer URL
advplyr Nov 5, 2023
e140897
Add match existing user by and auto register settings and UI
advplyr Nov 8, 2023
ee75d67
Matching user by openid sub, email or username based on server settin…
advplyr Nov 8, 2023
078cb08
Merge branch 'master' into auth_passportjs
advplyr Nov 10, 2023
237fe84
Add new API endpoint for updating auth-settings and update passport a…
advplyr Nov 10, 2023
557ef2e
Update /auth/openid endpoints for correct PKCE handling
advplyr Nov 11, 2023
1ad6722
Remove google-oauth passport strategy
advplyr Nov 11, 2023
fb48636
Openid auth failures redirect to login page with error message.
advplyr Nov 11, 2023
56c574c
Update package-lock
advplyr Nov 19, 2023
4c2c320
Remove global CORS for api endpoints and setup temp CORS check for eb…
advplyr Nov 19, 2023
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
20 changes: 20 additions & 0 deletions client/assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,24 @@ Bookshelf Label

.no-bars .Vue-Toastification__container.top-right {
padding-top: 8px;
}

.abs-btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}

.abs-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}

.abs-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
5 changes: 5 additions & 0 deletions client/components/app/ConfigSideNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export default {
id: 'config-rss-feeds',
title: this.$strings.HeaderRSSFeeds,
path: '/config/rss-feeds'
},
{
id: 'config-authentication',
title: this.$strings.HeaderAuthentication,
path: '/config/authentication'
}
]

Expand Down
24 changes: 2 additions & 22 deletions client/components/ui/Btn.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<template>
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
<nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</nuxt-link>
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
<button v-else class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
Expand Down Expand Up @@ -72,23 +72,3 @@ export default {
mounted() {}
}
</script>

<style scoped>
.btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
.btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
button:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
</style>
1 change: 1 addition & 0 deletions client/pages/config.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default {
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
else if (pageName === 'email') return this.$strings.HeaderEmail
else if (pageName === 'authentication') return this.$strings.HeaderAuthentication
}
return this.$strings.HeaderSettings
}
Expand Down
229 changes: 229 additions & 0 deletions client/pages/config/authentication.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<template>
<div>
<app-settings-content :header-text="$strings.HeaderAuthentication">
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
<div class="flex items-center">
<ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" />
<p class="text-lg pl-4">Password Authentication</p>
</div>
</div>
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
<div class="flex items-center">
<ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" />
<p class="text-lg pl-4">OpenID Connect Authentication</p>
</div>

<transition name="slide">
<div v-if="enableOpenIDAuth" class="flex flex-wrap pt-4">
<div class="w-full flex items-center mb-2">
<div class="flex-grow">
<ui-text-input-with-label ref="issuerUrl" v-model="newAuthSettings.authOpenIDIssuerURL" :disabled="savingSettings" :label="'Issuer URL'" />
</div>
<div class="w-36 mx-1 mt-[1.375rem]">
<ui-btn class="h-[2.375rem] text-sm inline-flex items-center justify-center w-full" type="button" :padding-y="0" :padding-x="4" @click.stop="autoPopulateOIDCClick">
<span class="material-icons text-base">auto_fix_high</span>
<span class="whitespace-nowrap break-keep pl-1">Auto-populate</span></ui-btn
>
</div>
</div>

<ui-text-input-with-label ref="authorizationUrl" v-model="newAuthSettings.authOpenIDAuthorizationURL" :disabled="savingSettings" :label="'Authorize URL'" class="mb-2" />

<ui-text-input-with-label ref="tokenUrl" v-model="newAuthSettings.authOpenIDTokenURL" :disabled="savingSettings" :label="'Token URL'" class="mb-2" />

<ui-text-input-with-label ref="userInfoUrl" v-model="newAuthSettings.authOpenIDUserInfoURL" :disabled="savingSettings" :label="'Userinfo URL'" class="mb-2" />

<ui-text-input-with-label ref="jwksUrl" v-model="newAuthSettings.authOpenIDJwksURL" :disabled="savingSettings" :label="'JWKS URL'" class="mb-2" />

<ui-text-input-with-label ref="logoutUrl" v-model="newAuthSettings.authOpenIDLogoutURL" :disabled="savingSettings" :label="'Logout URL'" class="mb-2" />

<ui-text-input-with-label ref="openidClientId" v-model="newAuthSettings.authOpenIDClientID" :disabled="savingSettings" :label="'Client ID'" class="mb-2" />

<ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" />

<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="'Button Text'" class="mb-2" />

<div class="flex items-center pt-1 mb-2">
<div class="w-44">
<ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" label="Match existing users by" :disabled="savingSettings" />
</div>
<p class="pl-4 text-sm text-gray-300 mt-5">Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider</p>
</div>

<div class="flex items-center py-4 px-1">
<ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" />
<p id="auto-redirect-toggle" class="pl-4">Auto Launch</p>
<p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the login page</p>
</div>

<div class="flex items-center py-4 px-1">
<ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" />
<p id="auto-register-toggle" class="pl-4">Auto Register</p>
<p class="pl-4 text-sm text-gray-300">Automatically create new users after logging in</p>
</div>
</div>
</transition>
</div>
<div class="w-full flex items-center justify-end p-4">
<ui-btn color="success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn>
</div>
</app-settings-content>
</div>
</template>

<script>
export default {
async asyncData({ store, redirect, app }) {
if (!store.getters['user/getIsAdminOrUp']) {
redirect('/')
return
}

const authSettings = await app.$axios.$get('/api/auth-settings').catch((error) => {
console.error('Failed', error)
return null
})
if (!authSettings) {
redirect('/config')
return
}
return {
authSettings
}
},
data() {
return {
enableLocalAuth: false,
enableOpenIDAuth: false,
savingSettings: false,
newAuthSettings: {}
}
},
computed: {
authMethods() {
return this.authSettings.authActiveAuthMethods || []
},
matchingExistingOptions() {
return [
{
text: 'Do not match',
value: null
},
{
text: 'Match by email',
value: 'email'
},
{
text: 'Match by username',
value: 'username'
}
]
}
},
methods: {
autoPopulateOIDCClick() {
if (!this.newAuthSettings.authOpenIDIssuerURL) {
this.$toast.error('Issuer URL required')
return
}
// Remove trailing slash
let issuerUrl = this.newAuthSettings.authOpenIDIssuerURL
if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1)

// If the full config path is on the issuer url then remove it
if (issuerUrl.endsWith('/.well-known/openid-configuration')) {
issuerUrl = issuerUrl.replace('/.well-known/openid-configuration', '')
this.newAuthSettings.authOpenIDIssuerURL = this.newAuthSettings.authOpenIDIssuerURL.replace('/.well-known/openid-configuration', '')
}

this.$axios
.$get(`/auth/openid/config?issuer=${issuerUrl}`)
.then((data) => {
if (data.issuer) this.newAuthSettings.authOpenIDIssuerURL = data.issuer
if (data.authorization_endpoint) this.newAuthSettings.authOpenIDAuthorizationURL = data.authorization_endpoint
if (data.token_endpoint) this.newAuthSettings.authOpenIDTokenURL = data.token_endpoint
if (data.userinfo_endpoint) this.newAuthSettings.authOpenIDUserInfoURL = data.userinfo_endpoint
if (data.end_session_endpoint) this.newAuthSettings.authOpenIDLogoutURL = data.end_session_endpoint
if (data.jwks_uri) this.newAuthSettings.authOpenIDJwksURL = data.jwks_uri
})
.catch((error) => {
console.error('Failed to receive data', error)
const errorMsg = error.response?.data || 'Unknown error'
this.$toast.error(errorMsg)
})
},
validateOpenID() {
let isValid = true
if (!this.newAuthSettings.authOpenIDIssuerURL) {
this.$toast.error('Issuer URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDAuthorizationURL) {
this.$toast.error('Authorize URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDTokenURL) {
this.$toast.error('Token URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDUserInfoURL) {
this.$toast.error('Userinfo URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDJwksURL) {
this.$toast.error('JWKS URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDClientID) {
this.$toast.error('Client ID required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDClientSecret) {
this.$toast.error('Client Secret required')
isValid = false
}
return isValid
},
async saveSettings() {
if (!this.enableLocalAuth && !this.enableOpenIDAuth) {
this.$toast.error('Must have at least one authentication method enabled')
return
}

if (this.enableOpenIDAuth && !this.validateOpenID()) {
return
}

this.newAuthSettings.authActiveAuthMethods = []
if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local')
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')

this.savingSettings = true
this.$axios
.$patch('/api/auth-settings', this.newAuthSettings)
.then((data) => {
this.$store.commit('setServerSettings', data.serverSettings)
this.$toast.success('Server settings updated')
})
.catch((error) => {
console.error('Failed to update server settings', error)
this.$toast.error('Failed to update server settings')
})
.finally(() => {
this.savingSettings = false
})
},
init() {
this.newAuthSettings = {
...this.authSettings
}
this.enableLocalAuth = this.authMethods.includes('local')
this.enableOpenIDAuth = this.authMethods.includes('openid')
}
},
mounted() {
this.init()
}
}
</script>

Loading