Skip to content

Commit

Permalink
feat!: Add OIDC login flow
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Previously available config options have been moved
under the `jwt` key. See `README.md` for an example.
  • Loading branch information
AgathaSorceress committed Dec 15, 2023
1 parent 41ac445 commit ad80b6a
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 94 deletions.
39 changes: 28 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![PyPI - Version](https://img.shields.io/pypi/v/synapse-token-authenticator.svg)](https://pypi.org/project/synapse-token-authenticator)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/synapse-token-authenticator.svg)](https://pypi.org/project/synapse-token-authenticator)

Synapse Token Authenticator is a synapse auth provider which allows for token authentication (and optional registration) using JWTs (Json Web Tokens).
Synapse Token Authenticator is a synapse auth provider which allows for token authentication (and optional registration) using JWTs (Json Web Tokens) and OIDC.

-----

Expand All @@ -25,20 +25,33 @@ pip install synapse-token-authenticator
## Configuration
Here are the available configuration options:
```yaml
# provide only one of secret, keyfile
secret: symetrical secret
keyfile: path to asymetrical keyfile

# Algorithm of the tokens, defaults to HS512
#algorithm: HS512
# Allow registration of new users using these tokens, defaults to false
#allow_registration: false
# Require tokens to have an expiry set, defaults to true
#require_expiry: true
jwt:
# provide only one of secret, keyfile
secret: symetrical secret
keyfile: path to asymetrical keyfile

# Algorithm of the tokens, defaults to HS512 (optional)
algorithm: HS512
# Allow registration of new users, defaults to false (optional)
allow_registration: false
# Require tokens to have an expiry set, defaults to true (optional)
require_expiry: true
oidc:
issuer: "https://idp.example.com"
client_id: "<IDP client id>"
client_secret: "<IDP client secret>"
project_id: # TODO: improve docs
organization_id: # TODO: improve docs
# Limits access to specified clients. Allows any client if not set (optional)
allowed_client_ids: ['foo', 'bar'] # TODO: better examples
# Allow registration of new users, defaults to false (optional)
allow_registration: false
```
It is recommended to have `require_expiry` set to `true` (default). As for `allow_registration`, it depends on usecase: If you only want to be able to log in *existing* users, leave it at `false` (default). If nonexistant users should be simply registered upon hitting the login endpoint, set it to `true`.

## Usage

### JWT Authentication
First you have to generate a JWT with the correct claims. The `sub` claim is the localpart or full mxid of the user you want to log in as. Be sure that the algorithm and secret match those of the configuration. An example of the claims is as follows:
```json
{
Expand All @@ -59,6 +72,10 @@ Next you need to post this token to the `/login` endpoint of synapse. Be sure th
}
```

### OIDC Authentication

<!-- TODO: write a summary of the notion page here -->

## Testing

The tests uses twisted's testing framework trial, with the development
Expand Down
10 changes: 4 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ dependencies = [
"pytest",
"pytest-cov",
"mock",
"matrix-synapse"
"matrix-synapse",
"ruff",
]
[tool.hatch.envs.default.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=synapse_token_authenticator --cov=tests"
format = "black ."
lint = "ruff check ."

[tool.hatch.envs.ci.scripts]
format = "black --check ."
Expand All @@ -54,8 +56,4 @@ parallel = true
omit = []

[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
70 changes: 70 additions & 0 deletions synapse_token_authenticator/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os


class TokenAuthenticatorConfig:
"""
Parses and validates the provided config dictionary.
"""

def __init__(self, other: dict):
if jwt := other.get("jwt"):

class JwtConfig:
def __init__(self, other: dict):
self.secret: str | None = other.get("secret")
self.keyfile: str | None = other.get("keyfile")

self.algorithm: str = other.get("algorithm", "HS512")
self.allow_registration: bool = other.get(
"allow_registration", False
)
self.require_expiry: bool = other.get("require_expiry", True)

self.jwt = JwtConfig(jwt)
self.verify_jwt()

if oidc := other.get("oidc"):

class OIDCConfig:
def __init__(self, other: dict):
try:
self.issuer: str = other["issuer"]
self.client_id: str = other["client_id"]
self.client_secret: str = other["client_secret"]
self.project_id: str = other["project_id"]
self.organization_id: str = other["organization_id"]
except KeyError as error:
raise Exception(f"Config option must be set: {error.args[0]}")

self.allowed_client_ids: str | None = other.get(
"allowed_client_ids"
)

self.allow_registration: bool = other.get(
"allow_registration", False
)

self.oidc = OIDCConfig(oidc)

def verify_jwt(self):
if self.jwt.secret is None and self.jwt.keyfile is None:
raise Exception("Missing secret or keyfile")
if self.jwt.keyfile is not None and not os.path.exists(self.jwt.keyfile):
raise Exception("Keyfile doesn't exist")

if self.jwt.algorithm not in [
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"ES512",
"PS256",
"PS384",
"PS512",
"EdDSA",
]:
raise Exception(f"Unknown algorithm: '{self.jwt.algorithm}'")
Loading

0 comments on commit ad80b6a

Please sign in to comment.