Skip to content
Open
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
177 changes: 177 additions & 0 deletions docs/adr/004-oidc-token_ttl_improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# ADR: OIDC Token TTL Improvements

## Status

**Proposed**

## Context

In multi-application deployments where Twake Workplace serves applications (like Twake Mail) in iframes,
there is a fundamental disconnect between cozy-stack session management and OIDC token validity.

#### Real-World Scenario: pommes.fr Deployment

```
Domain Structure:
├── auth.pommes.fr ← OIDC Provider
└── *.workplace.pommes.fr ← Twake Workplace apps
├── home.workplace.pommes.fr
└── Twake Mail.workplace.pommes.fr (iframe)
```

**Problem Flow:**

```mermaid
sequenceDiagram
participant User
participant Cozy as Cozy Home
participant Twake Mail as Twake Mail (iframe)
participant Stack as Cozy Stack
participant OIDC

User->>Cozy: Login via OIDC
Cozy->>OIDC: Authenticate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that the current flow is this one?

Because Cozy Home will never communicate with the OIDC. It should be cozy-stack

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it's a super-simplified one just to illustrate the problem. It doesn't interracts with OIDC except redirect to sign-up to do the auth

OIDC-->>Cozy: Tokens (TTL: 1 hour)
Cozy->>Stack: Create session
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create session is not done by Cozy Home.

Cozy Stack will generate a token and pass this token to cozy home

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I've mixed app with mobile app flow

Stack-->>Cozy: Session (TTL: 30 days)

Note over User,OIDC: Time passes... OIDC token expires

User->>Twake Mail: Open Twake Mail in iframe
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be User->>Twake Mail Iframe ->> Twake Mail

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if Twake Mail Iframe is a cozy app, yes

Twake Mail->>Stack: API request
Stack-->>Twake Mail: 401 Unauthorized
Twake Mail->>OIDC: Redirect to login
OIDC--xTwake Mail: ❌ Blocked! CSP doesn't allow iframe

Note over Twake Mail: User stuck - can't authenticate
```

#### Root Cause

Cozy-stack sessions (30 days) are completely independent of OIDC token validity (typically 1 day)

#### Impact

- Users appear logged into Cozy but embedded apps fail silently
- OIDC provider login page cannot load in iframe (CSP `frame-ancestors` restriction)
- Poor user experience with no clear path to resolution


### Current Implementation

The cozy-stack currently supports OpenID Connect (OIDC) for delegated authentication. When a user authenticates via OIDC, the stack:

1. Exchanges the authorization code for tokens (`access_token`, `id_token`, optionally `refresh_token`)
2. Uses the `access_token` once to call the UserInfo endpoint
3. Extracts the `sid` (session ID) from the `id_token` for logout support
4. Discards all OIDC tokens immediately after use
5. Issues its own cozy-stack OAuth tokens to the client

The current token response parsing only captures two fields:

```go
type tokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
}
```

### Current Limitations

| Limitation | Impact |
|------------|--------|
| OIDC `refresh_token` not captured | Cannot refresh OIDC access token when it expires |
| OIDC `expires_in` not captured | No awareness of token TTL |
| OIDC `access_token` not stored | Cannot make subsequent calls to OIDC-protected APIs on behalf of user |
| Session TTL independent of OIDC | Sessions valid long after OIDC token expires |
| No lifecycle validation | Token validity not checked after initial auth |
| Back-channel logout deletes ALL sessions | Should use `sid` for per-device logout |


## Proposal

### Extend Token Response Parsing

Update the `tokenResponse` struct to capture all relevant fields:

```go
type tokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
TokenType string `json:"token_type,omitempty"`
}
```

#### Store OIDC Tokens on OAuth Client

Extend the `oauth.Client` struct to store OIDC tokens:

```go
type Client struct {
// ... existing fields ...

// OIDC token storage
OIDCSessionID string `json:"oidc_session_id,omitempty"` // Existing
OIDCAccessToken string `json:"oidc_access_token,omitempty"` // New
OIDCRefreshToken string `json:"oidc_refresh_token,omitempty"` // New
OIDCTokenExpiresAt *time.Time `json:"oidc_token_expires_at,omitempty"` // New
}
```

#### Store OIDC Token Expiry on Session

Extend the `session.Session` struct:

```go
type Session struct {
// ... existing fields ...
SID string `json:"sid,omitempty"` // Existing
OIDCTokenExpiresAt *time.Time `json:"oidc_token_expires_at,omitempty"` // New
}
```

### Session TTL Alignment

#### Session Validity Middleware

Add HTTP middleware that checks OIDC token expiry on each request:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on the expires_in attribute right? Not by doing an http call right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or based on introspect?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, base on expires_in

if the token is expired, return a 401 Unauthorized response with an `oidc_token_expired` error indicating re-authentication is required.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This http middleware will only work for "cozy apps" not for "iframed ones".

But let's focus on theses cozy-apps first

I don't think cozy-stack should return a 401 Unauthorized.

Because a cozy application will never know what to do with oidc_token_expired.

Why the stack do not refresh the oidc_token based on the oidc_refresh_token. Like that, there is nothing to do for the client app in 95% of the time.

We'll still have issue if the oidc_token is expired and the cozy stack is unable to refresh the token (because refresh token is not valid anymore).

Then in this case, maybe cozy-stack should redirect to login page directly?

Now let's talk about "iframed" app.

I'm on an iframed app for a long time without any activities. I do not refresh my browser. So I've my cozy bar authenticated because I didn't do any http request throught the stack. I start navigating in my iframed app, and this iframed app is not able to refresh its own token, then this iframed app will redirect me to the sso. And we'll be in the use case we want to fix: not displaying the sso page in the iframe.

Maybe we can do some magic from cozy-bridge (cc @zatteo) to detect the redirection to the SSO and do stuff at the cozy-app level (redirect to the SSO on top window).

But maybe also, the cozy front end application can be aware of the expired time? Based on its own token with an expired_at attribute? In this case, cozy-bar can be able to make an http request and then run into this http middleware and so on?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we don't have a problem when the token expires in the stack, we have problem when the token expires in twake male, and twake male doesn't refresh the session.

Yes, cozy stack I think even should do this, but it's just more changes to OIDC flow. But we don't know at what moment we should redirect to OIDC flow. we can't send 322 loging redirect to every post instead of 401.

But the bigget problem is that you've mentioned, and it's not solved with this ADR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can do some magic from cozy-bridge (cc @zatteo) to detect the redirection to the SSO and do stuff at the cozy-app level (redirect to the SSO on top window).

We won't be able to monitor from parent window URL change from twake mail if it redirects to SSO because we are not same origin so no access to url updates from iframe. But otherwise yes we can do stuff.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we don't have a problem when the token expires in the stack, we have problem when the token expires in twake male, and twake male doesn't refresh the session.

That's not totally true. TMail refresh the session, but sometimes your refresh token is expired. So you just can't refresh.

I still think this iframe stuff is not the right thing to do. We should let the app (stack / tmail / tchat) identify itself, and then inject the cozy-bar. Like that, no more issue like this one.

I'll have a talk with @zatteo & @poupotte

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Crash-- it's not 100% solution, since Twake Mail app has it's own auth oidc flow, but we can also can listen an event form stack in the bridge app when session is expired. We should be carefull at this step, to have the same oidc refresh flow and timeouts for both app(all settings shoudl be the same or mo restrictive on the stack side)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can do something:

  • cozy-bridge can expose a method which can be called by the embedded when refreshing
  • and then call a logout from the stack & start a full oidc flow

I still think that, regarding this embedded/embedder approach, we might be going in the wrong direction.
We're considering adding a lot of methods and tools to work around something the application handle natively.
We should continue exploring alternatives without iframes.

Copy link
Copy Markdown
Contributor

@Crash-- Crash-- May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But let's fix at least the cozy's app issue. This ADR should fix that. Can you update it to target this objective please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "cozy's app issue"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • check token validity during the use of a cozy application.
  • check token validity when we open a new cozy app

It'll fix these scenarios :

  • I've my cozy home page opened since a lot of time, and then I click on the mail application (iframed). => Cozy stack should detect that my current session is ended and then redirect me to the SSO. I will log again, and then, I'll come back to my mail app (iframed) and I'll be connected.
  • I'm writing things on my notes app, or sometimes on my only office document, then, if my session is expired, cozy stack should refresh it (without being disconnected & without losing my content). Then after, I can open my iframed app I'll be connected.


### Back-Channel Logout Fix

Per-Device Logout Using SID

### Extended Configuration Options

```yaml
authentication:
the-context-name:
oidc:
# ... existing config ...

# Token storage (Part A)
store_tokens: true # Enable OIDC token storage (default: false)

# Session alignment (Part C)
align_session_ttl: true # Tie session validity to OIDC token (default: false)
```

## Alternatives

### Store Tokens in Session Instead of OAuth Client

Store OIDC tokens in the `session.Session` document instead of `oauth.Client`.


### Encrypt Stored Tokens

Encrypt OIDC tokens before storing in CouchDB.


## Decision

## Consequences

Desktop/Mobile apps will have token with the same short TTL
32 changes: 32 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Architecture Decision Records (ADR)

This directory contains Architecture Decision Records (ADRs) for the cozy-stack project.

## What is an ADR?

An Architecture Decision Record (ADR) is a document that captures an important architectural decision made along with its context and consequences.

## ADR Template

Each ADR should include:

- **Status**: Proposed, Accepted, Deprecated, or Superseded
- **Context**: The situation and problem we are facing
- **Proposal**: The proposed solution
- **Alternatives**: Other options considered
- **Decision**: The final decision and rationale
- **Consequences**: The resulting effects (positive and negative)

## Index

| ADR | Title | Status |
|-----|-------|--------|
| [004](004-oidc-token_ttl_improvements.md) | OIDC Token Management Improvements | Proposed |

## Creating a New ADR

1. Copy the template or an existing ADR
2. Use the next sequential number: `NNNN-short-title.md`
3. Fill in all sections
4. Update this README index
5. Submit for review
Loading