Skip to content

Commit

Permalink
feat: delegate auth in embed mode
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasHirt committed Nov 28, 2023
1 parent 22ea1b4 commit 430d807
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 27 deletions.
28 changes: 25 additions & 3 deletions docs/embed-mode/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The ownCloud Web can be consumed by another application in a stripped down versi

## Getting started

To integrate ownCloud Web into your application, add an iframe element pointing to your ownCloud Web deployed instance with additional query parameter `mode=embed`.
To integrate ownCloud Web into your application, add an iframe element pointing to your ownCloud Web deployed instance with additional query parameter `embed=true`.

```html
<iframe src="<web-url>?mode=embed"></iframe>
Expand All @@ -41,7 +41,7 @@ To maintain uniformity and ease of handling, each event encapsulates the same st
### Example

```html
<iframe src="https://my-owncloud-web-instance?mode=embed"></iframe>
<iframe src="https://my-owncloud-web-instance?embed=true"></iframe>

<script>
function selectEventHandler(event) {
Expand All @@ -65,7 +65,7 @@ By default, the Embed mode allows users to select resources. In certain cases (e
### Example
```html
<iframe src="https://my-owncloud-web-instance?mode=embed&embed-target=location"></iframe>
<iframe src="https://my-owncloud-web-instance?embed=true&embed-target=location"></iframe>
<script>
function selectEventHandler(event) {
Expand All @@ -81,3 +81,25 @@ By default, the Embed mode allows users to select resources. In certain cases (e
window.addEventListener('message', selectEventHandler)
</script>
```
## Delegate authentication
If you already have a valid `access_token` that can be used to call the API from within the Embed mode and do not want to force the user to authenticate again, you can delegate the authentication. Delegating authentication will disable internal login form in ownCloud Web and will instead use events to obtain the token and update it.
### Configuration
To allow authentication delegation, you need to set a config option `options.embed.delegateAuthentication` to `true`. This can be achieved via query parameter `embed-delegate-authentication=true`. Because we are using `postMessage` method to communicate across different origins, it is best practice to verify that the event originated in known origin and not from some malicious site. To allow this check, you need to set config option `options.embed.delegateAuthenticationOrigin` by adding query parameter `embed-delegate-authentication-origin=my-origin`. The value of this parameter will be compared against the `MessageEvent.origin` value and if they do not match, the token will not be saved.
### Events
#### Opening Embed mode
As we already mentioned, we're using `postMessage` method to allow communication between the Embed mode and parent application. When Embed mode is opened for the first time, user gets redirected to `/web-oidc-callback` page where a message with payload `{ name: 'owncloud-embed:request-token', data: undefined }` is sent to request the `access_token` from the parent application. The parent application should set an event listener before opening the Embed mode and once received, it should send a message with payload `{ name: 'owncloud-embed:refresh-token', data: { access_token: '<bearer-token>' } }`. Once Embed mode receives this message, it will save the token in the store and will automatically authenticate the user.
{{< hint info >}}
To save unnecessary duplication of messages with only different names, the name in the message payload above is exactly the same as when refreshing the token.
{{< /hint >}}
#### Refreshing token
When authentication is delegated, the automatic renewal of token inside of ownCloud Web is disabled. In order to refresh the token, a listener is created which awaits a message with payload `{ name: 'owncloud-embed:refresh-token', data: { access_token: '<bearer-token>' } }`. Token will then be automatically replaced inside of the Embed mode.
25 changes: 24 additions & 1 deletion packages/web-pkg/src/composables/embedMode/useEmbedMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export const useEmbedMode = () => {
() => store.getters.configuration.options.embed?.messagesOrigin
)

const isDelegatingAuthentication = computed<boolean>(
() => isEnabled.value && store.getters.configuration.options.embed.delegateAuthentication
)

const delegateAuthenticationOrigin = computed<string | null>(
() => store.getters.configuration.options.embed.delegateAuthenticationOrigin
)

const postMessage = <Payload>(name: string, data?: Payload): void => {
const options: WindowPostMessageOptions = {}

Expand All @@ -24,5 +32,20 @@ export const useEmbedMode = () => {
window.parent.postMessage({ name, data }, options)
}

return { isEnabled, isLocationPicker, messagesTargetOrigin, postMessage }
const verifyDelegatedAuthenticationOrigin = (eventOrigin: string): boolean => {
if (!delegateAuthenticationOrigin.value) {
return true
}

return delegateAuthenticationOrigin.value === eventOrigin
}

return {
isEnabled,
isLocationPicker,
messagesTargetOrigin,
isDelegatingAuthentication,
postMessage,
verifyDelegatedAuthenticationOrigin
}
}
18 changes: 18 additions & 0 deletions packages/web-pkg/src/configuration/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ export class ConfigurationManager {
set(this.optionsConfiguration, 'isRunningOnEos', get(options, 'isRunningOnEos', false))

set(this.optionsConfiguration, 'ocm.openRemotely', get(options, 'ocm.openRemotely', false))

set(this.optionsConfiguration, 'embed.enabled', get(options, 'embed.enabled', false))
set(this.optionsConfiguration, 'embed.target', get(options, 'embed.target', 'resources'))
set(
this.optionsConfiguration,
'embed.messagesOrigin',
get(options, 'embed.messagesOrigin', null)
)
set(
this.optionsConfiguration,
'embed.delegateAuthentication',
get(options, 'embed.delegateAuthentication', false)
)
set(
this.optionsConfiguration,
'embed.delegateAuthenticationOrigin',
get(options, 'embed.delegateAuthenticationOrigin', null)
)
}

get options(): OptionsConfiguration {
Expand Down
4 changes: 3 additions & 1 deletion packages/web-pkg/src/configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export interface OptionsConfiguration {
embed?: {
enabled?: boolean
target?: string
messagesOrigin?: string
messagesOrigin?: string | null
delegateAuthentication?: boolean
delegateAuthenticationOrigin?: string | null
}
}

Expand Down
52 changes: 38 additions & 14 deletions packages/web-runtime/src/container/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ import { Resource } from '@ownclouders/web-client'
import PQueue from 'p-queue'
import { extractNodeId, extractStorageId } from '@ownclouders/web-client/src/helpers'

const getEmbedConfigFromQuery = (
doesEmbedEnabledOptionExists: boolean
): RawConfig['options']['embed'] => {
const config: RawConfig['options']['embed'] = {}

if (!doesEmbedEnabledOptionExists) {
config.enabled = getQueryParam('embed') === 'true'
}

// Can enable location picker in embed mode
const embedTarget = getQueryParam('embed-target')

if (embedTarget) {
config.target = embedTarget
}

const delegateAuthentication = getQueryParam('embed-delegate-authentication')

if (delegateAuthentication) {
config.delegateAuthentication = delegateAuthentication === 'true'
}

const delegateAuthenticationOrigin = getQueryParam('embed-delegate-authentication-origin')

if (delegateAuthentication) {
config.delegateAuthenticationOrigin = delegateAuthenticationOrigin
}

return config
}

/**
* fetch runtime configuration, this step is optional, all later steps can use a static
* configuration object as well
Expand All @@ -55,21 +86,14 @@ export const announceConfiguration = async (path: string): Promise<RuntimeConfig
throw new Error(`config could not be parsed. ${error}`)
})) as RawConfig

if (
!rawConfig.options?.embed ||
!Object.prototype.hasOwnProperty.call(rawConfig.options.embed, 'enabled')
) {
rawConfig.options = {
...rawConfig.options,
embed: { ...rawConfig.options?.embed, enabled: getQueryParam('embed') === 'true' }
}
}

// Can enable location picker in embed mode
const embedTarget = getQueryParam('embed-target')
const embedConfig = getEmbedConfigFromQuery(
rawConfig.options?.embed &&
Object.prototype.hasOwnProperty.call(rawConfig.options.embed, 'enabled')
)

if (embedTarget) {
rawConfig.options.embed = { ...rawConfig.options.embed, target: embedTarget }
rawConfig.options = {
...rawConfig.options,
embed: { ...rawConfig.options?.embed, ...embedConfig }
}

configurationManager.initialize(rawConfig)
Expand Down
30 changes: 28 additions & 2 deletions packages/web-runtime/src/pages/oidcCallback.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
</template>

<script lang="ts">
import { computed, defineComponent, onMounted, ref, unref } from 'vue'
import { useRoute, useStore } from '@ownclouders/web-pkg'
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, unref } from 'vue'
import { useEmbedMode, useRoute, useStore } from '@ownclouders/web-pkg'
import { authService } from 'web-runtime/src/services/auth'
export default defineComponent({
name: 'OidcCallbackPage',
setup() {
const store = useStore()
const { isDelegatingAuthentication, postMessage, verifyDelegatedAuthenticationOrigin } =
useEmbedMode()
const error = ref(false)
Expand All @@ -35,6 +37,19 @@ export default defineComponent({
})
const route = useRoute()
const handleRequestedTokenEvent = (event: MessageEvent): void => {
if (verifyDelegatedAuthenticationOrigin(event.origin) === false) {
return
}
if (event.data?.name !== 'owncloud-embed:refresh-token') {
return
}
authService.signInCallback(event.data.data.access_token)
}
onMounted(() => {
if (unref(route).query.error) {
error.value = true
Expand All @@ -44,13 +59,24 @@ export default defineComponent({
return
}
if (unref(isDelegatingAuthentication)) {
postMessage<void>('owncloud-embed:request-access-token')
window.addEventListener('message', handleRequestedTokenEvent)
return
}
if (unref(route).path === '/web-oidc-silent-redirect') {
authService.signInSilentCallback()
} else {
authService.signInCallback()
}
})
onBeforeUnmount(() => {
window.removeEventListener('message', handleRequestedTokenEvent)
})
return {
error,
logoImg,
Expand Down
12 changes: 11 additions & 1 deletion packages/web-runtime/src/router/setupAuthGuard.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { extractPublicLinkToken, isIdpContext, isPublicLinkContext, isUserContext } from './index'
import { Router, RouteLocation } from 'vue-router'
import { contextRouteNameKey, queryItemAsString } from '@ownclouders/web-pkg'
import { contextRouteNameKey, queryItemAsString, useEmbedMode } from '@ownclouders/web-pkg'
import { authService } from '../services/auth/authService'

export const setupAuthGuard = (router: Router) => {
router.beforeEach(async (to, from) => {
const { isDelegatingAuthentication } = useEmbedMode()

if (from && to.path === from.path && !hasContextRouteNameChanged(to, from)) {
// note: except for the context route, query changes can never trigger re-init of the auth context
return true
Expand Down Expand Up @@ -34,13 +36,21 @@ export const setupAuthGuard = (router: Router) => {

if (isUserContext(router, to)) {
if (!store.getters['runtime/auth/isUserContextReady']) {
if (isDelegatingAuthentication.value) {
return { path: '/web-oidc-callback' }
}

return { path: '/login', query: { redirectUrl: to.fullPath } }
}
return true
}

if (isIdpContext(router, to)) {
if (!store.getters['runtime/auth/isIdpContextReady']) {
if (isDelegatingAuthentication.value) {
return { path: '/web-oidc-callback' }
}

return { path: '/login', query: { redirectUrl: to.fullPath } }
}
return true
Expand Down
27 changes: 25 additions & 2 deletions packages/web-runtime/src/services/auth/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,21 @@ export class AuthService {
/**
* Sign in callback gets called from the IDP after initial login.
*/
public async signInCallback() {
public async signInCallback(access_token?: string) {
try {
await this.userManager.signinRedirectCallback(this.buildSignInCallbackUrl())
if (
this.configurationManager.options.embed.enabled &&
this.configurationManager.options.embed.delegateAuthentication &&
access_token
) {
await this.userManager.updateContext(access_token, true)

// Setup a listener to handle token refresh
window.addEventListener('message', this.handleDelegatedTokenUpdate)
} else {
await this.userManager.signinRedirectCallback(this.buildSignInCallbackUrl())
}

const redirectRoute = this.router.resolve(this.userManager.getAndClearPostLoginRedirectUrl())
return this.router.replace({
path: redirectRoute.path,
Expand Down Expand Up @@ -269,6 +281,17 @@ export class AuthService {
this.store.dispatch('clearSettingsValues')
])
}

private handleDelegatedTokenUpdate(event: MessageEvent<string>): void {
if (
this.configurationManager.options.embed.delegateAuthenticationOrigin &&
event.origin !== this.configurationManager.options.embed.delegateAuthenticationOrigin
) {
return
}

this.userManager.updateContext(event.data, false)
}
}

export const authService = new AuthService()
6 changes: 5 additions & 1 deletion packages/web-runtime/src/services/auth/userManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ export class UserManager extends OidcUserManager {
post_logout_redirect_uri: buildUrl(router, '/'),
accessTokenExpiringNotificationTimeInSeconds: 10,
authority: '',
client_id: ''
client_id: '',

automaticSilentRenew:
!options.configurationManager.options.embed?.enabled ||
!options.configurationManager.options.embed.delegateAuthentication
}

if (options.configurationManager.isOIDC) {
Expand Down
4 changes: 3 additions & 1 deletion packages/web-runtime/src/store/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ const state = {
embed: {
enabled: false,
target: 'resources',
messagesOrigin: null
messagesOrigin: null,
delegateAuthentication: false,
delegateAuthenticationOrigin: null
}
}
}
Expand Down
Loading

0 comments on commit 430d807

Please sign in to comment.