Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Améliorations mineures sur l'API #101

Merged
merged 2 commits into from
Mar 12, 2024
Merged
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
Binary file added documentation/_static/keycloak-audience.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions documentation/developers/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# API publique

## Points d'accès

Une API publique est disponible sur `/api/meetings`. Cette API est utilisée par les greffons Thunderbird et Outlook.

Les réponses sont au format json sous la forme suivante :

```json
{
"meetings": [
{
"attendee_url": "https://example.tld/meeting/signin/1234/creator/5678/hash/27955e8c3a16ecadbb3cf61df7806a04ce6fd18c",
"moderator_url": "https://example.tld/meeting/signin/1234/creator/5678/hash/3e73643801d5013d389f8fc610e258aba38e597d",
"name": "Mon Séminaire"
}
]
}
```

## Authentification

L'authentification à l'API se fait en passant un jeton OIDC émis par le serveur d'identifié configuré dans `OIDC_ISSUER`.

### Tests

On peut tester le bon fonctionnement de l'API comme ceci (*en renseignant au préalable la variable `$TOKEN`*).

```bash
curl -s -H "Authorization:Bearer $TOKEN" https://example.tld/api/meetings
```

### Problèmes de connexion

#### Jeton manquant

Lorsque le jeton d'identification n'a pas été fourni dans la requête, l'API retourne des codes d'erreur HTTP 401.

#### Jeton expiré

Lorsque le jeton est expiré, l'API retourne des codes d'erreur HTTP 403.

#### Mauvaise audience du jeton

Lorsque l'audience du jeton est incorrecte, l'API retourne des codes d'erreur 403.
Dans les faits il faut s'assurer que le paramètre `aud` du jeton contient bien l'ID client OIDC de l'application, définie dans le paramètre `OIDC_CLIENT_ID` de l'application, et prenant par défaut la valeur `bbb-vision`.
On peut vérifier l'audience d'un token avec des outils tels que [jwt.io](https://jwt.io).

Par défaut, keycloak ne remplit pas l'audience du jeton avec l'ID client.
Il est nécessaire d'effectuer une configuration dans la console d'aministration de keycloak comme celle-ci (c'est un exemple, d'autres sont probablement possibles) :

1. Se rendre sur la console d'administration de keycloak
2. Se rendre dans le menu « Clients »
3. Sélectionner le client « bbb-visio »
4. Se rendre dans l'onglet « Mappers »
5. Cliquer sur le bouton « Create »
6. Remplir (par exemple) le formulaire comme ceci:
- Name: bbb-visio-audience
- Mapper Type: audience
- Included Custom Audience: bbb-visio

![keycloak](../_static/keycloak-audience.png)
7. Générer un nouveau token et vérifier qu'il contient bien la valeur `bbb-vision` dans le paramètre `aud`.
1 change: 1 addition & 0 deletions documentation/developers/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Cette documentation est à destination des développeurs.
.. toctree::
:maxdepth: 2

api
contributing
ci
dockerPersistence
Expand Down
2 changes: 2 additions & 0 deletions web/b3desk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,13 @@ def setup_endpoints(app):
import b3desk.endpoints.public
import b3desk.endpoints.join
import b3desk.endpoints.meetings
import b3desk.endpoints.api
import b3desk.endpoints.meeting_files

app.register_blueprint(b3desk.endpoints.public.bp)
app.register_blueprint(b3desk.endpoints.join.bp)
app.register_blueprint(b3desk.endpoints.meetings.bp)
app.register_blueprint(b3desk.endpoints.api.bp)
app.register_blueprint(b3desk.endpoints.meeting_files.bp)


Expand Down
28 changes: 28 additions & 0 deletions web/b3desk/endpoints/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from b3desk.models.users import get_or_create_user
from flask import Blueprint
from flask import request

from .. import auth


bp = Blueprint("api", __name__)


@bp.route("/api/meetings")
@auth.token_auth("default", scopes_required=["profile", "email"])
def api_meetings():
client = auth.clients["default"]
access_token = auth._parse_access_token(request)
userinfo = client.userinfo_request(access_token).to_dict()
user = get_or_create_user(userinfo)

return {
"meetings": [
{
"name": m.name,
"moderator_url": m.get_signin_url("moderator"),
"attendee_url": m.get_signin_url("attendee"),
}
for m in user.meetings
]
}
26 changes: 0 additions & 26 deletions web/b3desk/endpoints/meetings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from b3desk.models import db
from b3desk.models.meetings import get_quick_meeting_from_user_and_random_string
from b3desk.models.meetings import Meeting
from b3desk.models.users import get_or_create_user
from b3desk.models.users import User
from flask import abort
from flask import Blueprint
Expand Down Expand Up @@ -50,31 +49,6 @@ def meeting_mailto_params(meeting, role):
).replace("\n", "%0D%0A")


@bp.route("/api/meetings")
@auth.token_auth(provider_name="default")
def api_meetings():
# TODO: probably unused
if not auth.current_token_identity:
return redirect(url_for("public.index"))

info = {
"given_name": auth.current_token_identity["given_name"],
"family_name": auth.current_token_identity["family_name"],
"email": auth.current_token_identity["email"],
}
user = get_or_create_user(info)
return {
"meetings": [
{
"name": m.name,
"moderator_url": m.get_signin_url("moderator"),
"attendee_url": m.get_signin_url("attendee"),
}
for m in user.meetings
]
}


@bp.route("/meeting/mail", methods=["POST"])
def quick_mail_meeting():
#### Almost the same as quick meeting but we do not redirect to join
Expand Down
4 changes: 4 additions & 0 deletions web/b3desk/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from b3desk.models.users import get_or_create_user
from flask import abort
from flask import current_app
from flask import g
from flask import session
from flask_pyoidc.user_session import UserSession
Expand All @@ -12,6 +13,9 @@ def get_current_user():
user_session = UserSession(session)
info = user_session.userinfo
g.user = get_or_create_user(info)
current_app.logger.debug(
f"User authenticated with token: {user_session.access_token}"
)
return g.user


Expand Down
53 changes: 49 additions & 4 deletions web/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import threading
import time
import uuid
Expand All @@ -17,6 +18,21 @@
from b3desk.models import db


@pytest.fixture
def iam_user(iam_server):
iam_user = iam_server.models.User(
id="user_id",
emails=["[email protected]"],
given_name="Alice",
user_name="Alice_user_name",
family_name="Cooper",
)
iam_user.save()

yield iam_user
iam_user.delete()


@pytest.fixture
def iam_client(iam_server):
iam_client = iam_server.models.Client(
Expand All @@ -36,6 +52,28 @@ def iam_client(iam_server):
iam_client.delete()


@pytest.fixture
def iam_token(iam_server, iam_client, iam_user):
iam_token = iam_server.models.Token(
access_token="access_token_example",
audience=iam_client,
client=iam_client,
id="token_id",
issue_date=datetime.datetime.now(tz=datetime.timezone.utc),
lifetime=36000,
refresh_token="refresh_token_example",
revokation_date=None,
scope=["openid", "profile", "email"],
subject=iam_user,
token_id="token_id",
type="access_token",
)
iam_token.save()

yield iam_token
iam_token.delete()


@pytest.fixture
def configuration(tmp_path, iam_server, iam_client, smtpd):
smtpd.config.use_starttls = True
Expand Down Expand Up @@ -115,19 +153,23 @@ def meeting(client_app, user):


@pytest.fixture
def user(client_app):
def user(client_app, iam_user):
from b3desk.models.users import User

user = User(email="[email protected]", given_name="Alice", family_name="Cooper")
user = User(
email=iam_user.emails[0],
given_name=iam_user.given_name,
family_name=iam_user.family_name,
)
user.save()

yield user


@pytest.fixture
def authenticated_user(client_app, user):
def authenticated_user(client_app, user, iam_token, iam_server, iam_user):
with client_app.session_transaction() as session:
session["access_token"] = ""
session["access_token"] = iam_token.access_token
session["access_token_expires_at"] = ""
session["current_provider"] = "default"
session["id_token"] = ""
Expand All @@ -142,6 +184,9 @@ def authenticated_user(client_app, user):
}
session["refresh_token"] = ""

iam_server.login(iam_user)
iam_server.consent(iam_user)

yield user


Expand Down
109 changes: 109 additions & 0 deletions web/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import datetime


def test_api_meetings_nominal(client_app, user, meeting, iam_token):
res = client_app.get(
"/api/meetings", headers={"Authorization": f"Bearer {iam_token.access_token}"}
)
assert res.json["meetings"]
assert res.json["meetings"][0]["name"] == "meeting"
assert (
f"/meeting/signin/{meeting.id}/creator/{user.id}/hash/"
in res.json["meetings"][0]["moderator_url"]
)
assert (
f"/meeting/signin/{meeting.id}/creator/{user.id}/hash/"
in res.json["meetings"][0]["attendee_url"]
)


def test_api_meetings_no_token(client_app):
client_app.get("/api/meetings", status=401)


def test_api_meetings_invalid_token(client_app):
client_app.get(
"/api/meetings", headers={"Authorization": "Bearer invalid-token"}, status=403
)


def test_api_meetings_token_expired(client_app, iam_server, iam_client, iam_user, user):
iam_token = iam_server.models.Token(
access_token="access_token_example",
audience=iam_client,
client=iam_client,
id="token_id",
issue_date=datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc),
lifetime=36000,
refresh_token="refresh_token_example",
revokation_date=None,
scope=["openid", "profile", "email"],
subject=iam_user,
token_id="token_id",
type="access_token",
)
iam_token.save()

client_app.get(
"/api/meetings",
headers={"Authorization": f"Bearer {iam_token.access_token}"},
status=403,
)

iam_token.delete()


def test_api_meetings_client_id_missing_in_token_audience(
client_app, iam_server, iam_client, iam_user, user
):
iam_token = iam_server.models.Token(
access_token="access_token_example",
audience="some-other-audience",
client=iam_client,
id="token_id",
issue_date=datetime.datetime.now(tz=datetime.timezone.utc),
lifetime=36000,
refresh_token="refresh_token_example",
revokation_date=None,
scope=["openid", "profile", "email"],
subject=iam_user,
token_id="token_id",
type="access_token",
)
iam_token.save()

client_app.get(
"/api/meetings",
headers={"Authorization": f"Bearer {iam_token.access_token}"},
status=403,
)

iam_token.delete()


def test_api_meetings_missing_scope_in_token(
client_app, iam_server, iam_client, iam_user, user
):
iam_token = iam_server.models.Token(
access_token="access_token_example",
audience=iam_client,
client=iam_client,
id="token_id",
issue_date=datetime.datetime.now(tz=datetime.timezone.utc),
lifetime=36000,
refresh_token="refresh_token_example",
revokation_date=None,
scope=["openid"],
subject=iam_user,
token_id="token_id",
type="access_token",
)
iam_token.save()

client_app.get(
"/api/meetings",
headers={"Authorization": f"Bearer {iam_token.access_token}"},
status=403,
)

iam_token.delete()
Loading