0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-23 06:58:58 -05:00
penpot/docs/technical-guide/developer/subsystems/authentication.md
2024-10-30 13:30:02 +01:00

149 lines
6.8 KiB
Markdown

---
title: Authentication
---
# User authentication
Users in Penpot may register via several different methods (if enabled in the
configuration of the Penpot instance). We have implemented this as a series
of "authentication backends" in our code:
* **penpot**: internal registration with email and password.
* **ldap**: authentication over an external LDAP directory.
* **oidc**, **google**, **github**, **gitlab**: authentication over an external
service using the [OpenID Connect](https://openid.net/connect) protocol. We
have a generic handler, and other ones already preconfigured for popular
services.
The main logic resides in the following files:
```text
backend/src/app/rpc/mutations/profile.clj
backend/src/app/rpc/mutations/ldap.clj
backend/src/app/rpc/mutations/verify-token.clj
backend/src/app/http/oauth.clj
backend/src/app/http/session.clj
frontend/src/app/main/ui/auth/verify-token.cljs
```
We store in the user profiles in the database the auth backend used to register
first time (mainly for audit). A user may login with other methods later, if the
email is the same.
## Register and login
The code is organized to try to reuse functions and unify processes as much as
possible for the different auth systems.
### Penpot backend
When a user types an email and password in the basic Penpot registration page,
frontend calls <code class="language-clojure">:prepare-register-profile</code> method. It generates a "register
token", a temporary JWT token that includes the login data.
This is used in the second registration page, that finally calls
<code class="language-clojure">:register-profile</code> with the token and the rest of profile data. This function
is reused in all the registration methods, and it's responsible of creating the
user profile in the database. Then, it sends the confirmation email if using
penpot backend, or directly opens a session (see below) for othe methods or if
the user has been invited from a team.
The confirmation email has a link to <code class="language-clojure">/auth/verify-token</code>, that has a handler
in frontend, that is a hub for different kinds of tokens (registration email,
email change and invitation link). This view uses <code class="language-clojure">:verify-token</code> RPC call and
redirects to the corresponding page with the result.
To login with the penpot backend, the user simply types the email and password
and they are sent to <code class="language-clojure">:login</code> method to check and open session.
### OIDC backend
When the user press one of the "Log in with XXX" button, frontend calls
<code class="language-clojure">/auth/oauth/:provider</code> (provider is google, github or gitlab). The handler
generates a request token and redirects the user to the service provider to
authenticate in it.
If succesful, the provider redirects to the<code class="language-clojure">/auth/oauth/:provider/callback</code>.
This verifies the call with the request token, extracts another access token
from the auth response, and uses it to request the email and full name from the
service provider.
Then checks if this is an already registered profile or not. In the first case
it opens a session, and in the second one calls<code class="language-clojure">:register-profile</code> to create a
new user in the sytem.
For the known service providers, the addresses of the protocol endpoints are
hardcoded. But for a generic OIDC service, there is a discovery protocol to ask
the provider for them, or the system administrator may set them via configuration
variables.
### LDAP
Registration is not possible by LDAP (we use an external user directory managed
outside of Penpot). Typically when LDAP registration is enabled, the plain user
& password login is disabled.
When the user types their user & password and presses "Login with LDAP" button,
the <code class="language-clojure">:login-with-ldap</code> method is called. It connects with the LDAP service to
validate credentials and retrieve email and full name.
Similarly as the OIDC backend, it checks if the profile exists, and calls
<code class="language-clojure">:login</code> or <code class="language-clojure">:register-profile</code> as needed.
## Sessions
User sessions are created when a user logs in via any one of the backends. A
session token is generated (a JWT token that does not currently contain any data)
and returned to frontend as a cookie.
Normally the session is stored in a DB table with the information of the user
profile and the session expiration. But if a frontend connects to the backend in
"read only" mode (for example, to debug something in production with the local
devenv), sessions are stored in memory (may be lost if the backend restarts).
## Team invitations
The invitation link has a call to <code class="language-clojure">/auth/verify-token</code> frontend view (explained
above) with a token that includes the invited email.
When a user follows it, the token is verified and then the corresponding process
is routed, depending if the email corresponds to an existing account or not. The
<code class="language-clojure">:register-profile</code> or <code class="language-clojure">:login</code> services are used, and the invitation token is
attached so that the profile is linked to the team at the end.
## Handling unfinished registrations and bouncing users
All tokens have an expiration date, and when they are put in a permanent
storage, a garbage colector task visits it periodically to cleand old items.
Also our email sever registers email bounces and spam complaint reportings
(see <code class="language-text">backend/src/app/emails.clj</code>). When the email of one profile receives too
many notifications, it becames blocked. From this on, the user cannot login or
register with this email, and no message will be sent to it. If it recovers
later, it needs to be unlocked manually in the database.
## How to test in devenv
To test all normal registration process you can use the devenv [Mail
catcher](/technical-guide/developer/devenv/#email) utility.
To test OIDC, you need to register an application in one of the providers:
* [Github](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app)
* [Gitlab](https://docs.gitlab.com/ee/integration/oauth_provider.html)
* [Google](https://support.google.com/cloud/answer/6158849)
The URL of the app will be the devenv frontend: [http://localhost:3449]().
And then put the credentials in <code class="language-text">backend/scripts/repl</code> and
<code class="language-text">frontend/resources/public/js/config.js</code>.
Finally, to test LDAP, in the devenv we include a [test LDAP](https://github.com/rroemhild/docker-test-openldap)
server, that is already configured, and only needs to be enabled in frontend
<code class="language-text">config.js</code>:
```js
var penpotFlags = "enable-login-with-ldap";
```