-
-
Notifications
You must be signed in to change notification settings - Fork 79
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
add new @login endpoint to return available external login options #1757
base: main
Are you sure you want to change the base?
Changes from all commits
cf79461
3ff293b
783a079
cc1347c
6cf2636
98c0cf5
68ce3be
46f891e
879752e
53002d4
37c47e7
06be87c
8168d4a
961def7
3cd4daa
0eccce4
499d74a
c97c97b
4cc4288
fd67f94
1a0c196
861673f
7a9e378
83c802e
f52a73d
92ae900
49a018f
b5c5899
730e8d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
--- | ||
myst: | ||
html_meta: | ||
"description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." | ||
"property=og:description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." | ||
"property=og:title": "@login for external authentication links" | ||
"keywords": "Plone, plone.restapi, REST, API, login, authentication, external services" | ||
--- | ||
|
||
# Login for external authentication links | ||
|
||
It is common to use add-ons that allow logging in to your site using third party services. | ||
Such add-ons include using authentication services provided by KeyCloak, GitHub, or other OAuth2 or OpenID Connect enabled services. | ||
|
||
When you install one of these add-ons, it modifies the login process, directing the user to third party services. | ||
|
||
To expose the links provided by these add-ons, `plone.restapi` provides an adapter based service registration. | ||
It lets those add-ons know that the REST API can use those services to authenticate users. | ||
This will mostly be used by frontends that need to show the end user the links to those services. | ||
|
||
To achieve that, third party products need to register one or more adapters for the Plone site root object, providing the `plone.restapi.interfaces.IExternalLoginProviders` interface. | ||
|
||
In the adapter, the add-on needs to return the list of external links and some metadata, including the `id`, `title`, and name of the `plugin`. | ||
|
||
An example adapter would be the following, in a file named {file}`adapter.py`: | ||
|
||
```python | ||
from zope.component import adapter | ||
from zope.interface import implementer | ||
|
||
@adapter(IPloneSiteRoot) | ||
@implementer(IExternalLoginProviders) | ||
class MyExternalLinks: | ||
def __init__(self, context): | ||
self.context = context | ||
|
||
def get_providers(self): | ||
return [ | ||
{ | ||
"id": "myprovider", | ||
"title": "Provider", | ||
"plugin": "myprovider", | ||
"url": "https://some.example.com/login-url", | ||
}, | ||
{ | ||
"id": "github", | ||
"title": "GitHub", | ||
"plugin": "github", | ||
"url": "https://some.example.com/login-authomatic/github", | ||
}, | ||
] | ||
``` | ||
|
||
With the corresponding ZCML stanza, in the corresponding {file}`configure.zcml` file: | ||
|
||
```xml | ||
<adapter factory=".adapter.MyExternalLinks" name="my-external-links"/> | ||
``` | ||
|
||
The API request would be as follows: | ||
|
||
```{eval-rst} | ||
.. http:example:: curl httpie python-requests | ||
:request: ../../../src/plone/restapi/tests/http-examples/external_authentication_links.req | ||
``` | ||
|
||
The server will respond with a `Status 200` and the list of external providers: | ||
|
||
```{literalinclude} ../../../src/plone/restapi/tests/http-examples/external_authentication_links.resp | ||
:language: http | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add a `@login` endpoint to get external login services' links. @erral |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# -*- coding: utf-8 -*- | ||
from plone.restapi.interfaces import IExternalLoginProviders | ||
from plone.restapi.services import Service | ||
from zope.component import getAdapters | ||
|
||
|
||
class Login(Service): | ||
def reply(self): | ||
adapters = getAdapters((self.context,), IExternalLoginProviders) | ||
external_providers = [] | ||
for name, adapter in adapters: | ||
external_providers.extend(adapter.get_providers()) | ||
|
||
return {"options": external_providers} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
GET /plone/@login HTTP/1.1 | ||
Accept: application/json | ||
Authorization: Basic YWRtaW46c2VjcmV0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
HTTP/1.1 200 OK | ||
Content-Type: application/json | ||
|
||
{ | ||
"options": [ | ||
{ | ||
"id": "myprovider", | ||
"plugin": "myprovider", | ||
"title": "Provider", | ||
"url": "https://some.example.com/login-url" | ||
}, | ||
{ | ||
"id": "github", | ||
"plugin": "github", | ||
"title": "GitHub", | ||
"url": "https://some.example.com/login-authomatic/github" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -42,6 +42,10 @@ | |||||
from plone.app.testing import popGlobalRegistry | ||||||
from plone.app.testing import pushGlobalRegistry | ||||||
from plone.restapi.testing import register_static_uuid_utility | ||||||
from zope.component import provideAdapter | ||||||
from plone.restapi.interfaces import IExternalLoginProviders | ||||||
from Products.CMFPlone.interfaces import IPloneSiteRoot | ||||||
|
||||||
|
||||||
import collections | ||||||
import json | ||||||
|
@@ -86,6 +90,27 @@ | |||||
open_kw = {"newline": "\n"} | ||||||
|
||||||
|
||||||
class MyExternalLinks: | ||||||
def __init__(self, context): | ||||||
self.context = context | ||||||
|
||||||
def get_providers(self): | ||||||
return [ | ||||||
{ | ||||||
"id": "myprovider", | ||||||
"title": "Provider", | ||||||
"plugin": "myprovider", | ||||||
"url": "https://some.example.com/login-url", | ||||||
}, | ||||||
{ | ||||||
"id": "github", | ||||||
"title": "GitHub", | ||||||
"plugin": "github", | ||||||
"url": "https://some.example.com/login-authomatic/github", | ||||||
}, | ||||||
] | ||||||
|
||||||
|
||||||
def normalize_test_port(value): | ||||||
# When you run these tests in the Plone core development buildout, | ||||||
# the port number is random. Normalize this to the default port. | ||||||
|
@@ -227,6 +252,13 @@ def setUp(self): | |||||
super().setUp() | ||||||
self.document = self.create_document() | ||||||
alsoProvides(self.document, ITTWLockable) | ||||||
provideAdapter( | ||||||
MyExternalLinks, | ||||||
adapts=(IPloneSiteRoot,), | ||||||
provides=IExternalLoginProviders, | ||||||
name="test-external-links", | ||||||
) | ||||||
|
||||||
transaction.commit() | ||||||
|
||||||
def tearDown(self): | ||||||
|
@@ -787,6 +819,12 @@ def test_documentation_jwt_logout(self): | |||||
) | ||||||
save_request_and_response_for_docs("jwt_logout", response) | ||||||
|
||||||
def test_documentation_external_doc_links(self): | ||||||
response = self.api_session.get( | ||||||
f"{self.portal.absolute_url()}/@login", | ||||||
) | ||||||
save_request_and_response_for_docs("external_authentication_links", response) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With my rename of the docs file, should this be changed as well?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, this is for the test-generated request and response file. I would leave this as it is, because we already have |
||||||
|
||||||
def test_documentation_batching(self): | ||||||
folder = self.portal[ | ||||||
self.portal.invokeFactory("Folder", id="folder", title="Folder") | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be defined in plone.base instead of plone.restapi, so that an addon doesn't need to depend on plone.restapi just to register this adapter and provide information about what providers it supports?
On the other hand, that would make it harder to backport support for this feature to older versions of Plone.
I don't feel strongly about it but in general, plone.restapi's purpose is to provide API access to features that exist at a lower level and might also be accessed via other interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the whole idea of the endpoint is to avoid duplicated
@login
endpoints such the ones we have in pas.plugins.authomatic and pas.plugins.oidc to support volto-authomaticI agree that perhaps this should go in plone.base somewhere and integrate it with Plone's standard login page.
Perhaps we can open an issue in Plone and work on that later on?