|
1 | 1 | # Authentication
|
2 | 2 |
|
3 |
| -OpenAPI allows declaring security schemes and security requirements of operations. |
| 3 | +## Model |
4 | 4 |
|
5 |
| -Lapidary allows declaring methods that create or consume `httpx.Auth` objects. |
| 5 | +Lapidary allows API client authors to declare security requirements using maps that specify acceptable security schemes |
| 6 | +per request. OAuth2 schemes require specifying scopes, while other schemes use an empty list. |
6 | 7 |
|
7 |
| -## Basic auth |
| 8 | +For example, a call might require authentication with either two specific schemes together (auth1 and auth2) or another |
| 9 | +pair (auth3 and auth4): |
8 | 10 |
|
9 |
| -TODO |
| 11 | +```python |
| 12 | +security = [ |
| 13 | + { |
| 14 | + 'auth1': ['scope1'], |
| 15 | + 'auth2': ['scope1', 'scope2'], |
| 16 | + }, |
| 17 | + { |
| 18 | + 'auth3': ['scope3'], |
| 19 | + 'auth4': [], |
| 20 | + }, |
| 21 | +] |
| 22 | +``` |
| 23 | + |
| 24 | +Lapidary also supports optional authentication, allowing for certain operations, such as a login endpoint, to forego the |
| 25 | +global security requirements specified for the API: |
| 26 | + |
| 27 | +```python |
| 28 | +security = [ |
| 29 | + {'auth': []}, |
| 30 | + {}, # unauthenticated calls allowed |
| 31 | +] |
| 32 | +``` |
10 | 33 |
|
11 |
| -## Login endpoints |
| 34 | +You can also use this method to disable global security requirement for a particular operation (e.g. login endpoint). |
12 | 35 |
|
13 |
| -A `/login/` or `/authenticate/` endpoint that returns the token is quite common with simpler authentication schemes like http or apiKey, yet their support is poor in OpenAPI. There's no way to connect |
14 |
| -such endpoint to a security scheme as in the case of OIDC. |
| 36 | +## Usage |
15 | 37 |
|
16 |
| -A function that handles such an endpoint can declare that it returns an Auth object, but it's not obvious to the user of python API which security scheme the method returns. |
| 38 | +Lapidary handles security schemes through httpx.Auth instances, wrapped in `NamedAuth` tuple. |
| 39 | +You can define security requirements globally in the client `__init__()` or at the operation level with decorators, where |
| 40 | +operation-level declarations override global settings. |
| 41 | + |
| 42 | +Lapidary validates security requirements at runtime, ensuring that any method call is accompanied by the necessary |
| 43 | +authentication, as specified by its security requirements. This process involves matching the provided authentication |
| 44 | +against the declared requirements before proceeding with the request. To meet these requirements, the user must have |
| 45 | +previously configured the necessary Auth instances using lapidary_authenticate. |
17 | 46 |
|
18 | 47 | ```python
|
19 |
| -from httpx import Auth |
20 |
| -import pydantic |
21 |
| -from typing_extensions import Annotated, Self |
22 |
| - |
23 |
| -from lapidary.runtime import post, ClientBase, RequestBody, APIKeyAuth, Responses |
24 |
| - |
25 |
| - |
26 |
| -class LoginRequest(pydantic.BaseModel): |
27 |
| - ... |
28 |
| - |
29 |
| - |
30 |
| -class LoginResponse(pydantic.BaseModel): |
31 |
| - token: str |
32 |
| - |
33 |
| - |
34 |
| -class Client(ClientBase): |
35 |
| - @post('/login') |
36 |
| - def login( |
37 |
| - self: Self, |
38 |
| - *, |
39 |
| - body: Annotated[LoginRequest, RequestBody({'application/json': LoginRequest})], |
40 |
| - ) -> Annotated[ |
41 |
| - Auth, |
42 |
| - Responses({ |
43 |
| - '200': { |
44 |
| - 'application/json': Annotated[ |
45 |
| - LoginResponse, |
46 |
| - APIKeyAuth( |
47 |
| - in_='header', |
48 |
| - name='Authorization', |
49 |
| - format='Token {body.token}' |
50 |
| - ), |
51 |
| - ] |
52 |
| - } |
53 |
| - }), |
54 |
| - ]: |
55 |
| - """Authenticates with the "primary" security scheme""" |
56 |
| -``` |
| 48 | +from lapidary.runtime import * |
| 49 | +from lapidary.runtime.auth import HeaderApiKey |
| 50 | +from typing import Self, Annotated |
57 | 51 |
|
58 |
| -The top return Annotated declares the returned type, the inner one declares the processing steps for the actual response. |
59 |
| -First the response is parsed as LoginResponse, then that object is passed to ApiKeyAuth which is a callable object. |
60 | 52 |
|
61 |
| -The result of the call, in this case an Auth object, is returned by the `login` function. |
| 53 | +class MyClient(ClientBase): |
| 54 | + def __init__(self): |
| 55 | + super().__init__( |
| 56 | + base_url=..., |
| 57 | + security=[{'apiKeyAuth': []}], |
| 58 | + ) |
62 | 59 |
|
63 |
| -The innermost Annotated is not necessary from the python syntax standpoint. It's done this way since it kind of matches the semantics of Annotated, but it could be replaced with a simple tuple or other type in the future. |
| 60 | + @get('/api/operation', security=[{'admin_only': []}]) |
| 61 | + async def my_op(self: Self) -> ...: |
| 62 | + pass |
| 63 | + |
| 64 | + @post('/api/login', security=()) |
| 65 | + async def login( |
| 66 | + self: Self, |
| 67 | + user: Annotated[str, ...], |
| 68 | + password: Annotated[str, ...], |
| 69 | + ) -> ...: |
| 70 | + pass |
64 | 71 |
|
65 |
| -## Using auth tokens |
| 72 | +# User code |
| 73 | +async def main(): |
| 74 | + client = MyClient() |
66 | 75 |
|
67 |
| -OpenApi allows operations to declare a collection of alternative groups of security requirements. |
| 76 | + token = await client.login().token |
| 77 | + client.lapidary_authenticate(apiKeyAuth=HeaderApiKey(token)) |
| 78 | + await client.my_op() |
68 | 79 |
|
69 |
| -The second most trivial example (the first being no security) is a single required security scheme. |
70 |
| -```yaml |
71 |
| -security: |
72 |
| -- primary: [] |
| 80 | + # optionally |
| 81 | + client.lapidary_deauthenticate('apiKeyAuth') |
73 | 82 | ```
|
74 |
| -The name of the security scheme corresponds to a key in `components.securitySchemes` object. |
75 | 83 |
|
76 |
| -This can be represented as a simple parameter, named for example `primary_auth` and of type `httpx.Auth`. |
77 |
| -The parameter could be annotated as `Optional` if the security requirement is optional for the operation. |
| 84 | +`lapidary_authenticate` also accepts tuples of Auth instances with names, so this is possible: |
78 | 85 |
|
79 |
| -In case of multiple alternative groups of security requirements, it gets harder to properly describe which schemes are required and in what combination. |
| 86 | +```python |
| 87 | +def admin_auth(api_key: str) -> NamedAuth: |
| 88 | + return 'admin_only', HeaderApiKey(api_key) |
80 | 89 |
|
81 |
| -Lapidary takes all passed `httpx.Auth` parameters and passes them to `httpx.AsyncClient.send(..., auth=auth_params)`, leaving the responsibility to select the right ones to the user. |
82 | 90 |
|
83 |
| -If multiple `Auth` parameters are passed, they're wrapped in `lapidary.runtime.aauth.MultiAuth`, which is just reexported `_MultiAuth` from `https_auth` package. |
| 91 | +client.lapidary_authencticate(admin_auth('my token')) |
| 92 | +``` |
84 | 93 |
|
85 |
| -#### Example |
| 94 | +## De-registering Auth instances |
86 | 95 |
|
87 |
| -Auth object returned by the login operation declared in the above example can be used by another operation. |
| 96 | +To remove an auth instance, thereby allowing for unauthenticated calls or the use of alternative security schemes, |
| 97 | +lapidary_deauthenticate is used: |
88 | 98 |
|
89 | 99 | ```python
|
90 |
| -from httpx import Auth |
91 |
| -from typing import Annotated, Self |
92 |
| -
|
93 |
| -from lapidary.runtime import ClientBase, get, post |
94 |
| -
|
95 |
| -
|
96 |
| -class Client(ClientBase): |
97 |
| - @post('/login') |
98 |
| - def login( |
99 |
| - self: Self, |
100 |
| - body: ..., |
101 |
| - ) -> Annotated[ |
102 |
| - Auth, |
103 |
| - ... |
104 |
| - ]: |
105 |
| - """Authenticates with the "primary" security scheme""" |
106 |
| -
|
107 |
| - @get('/private') |
108 |
| - def private( |
109 |
| - self: Self, |
110 |
| - *, |
111 |
| - primary_auth: Auth, |
112 |
| - ): |
113 |
| - pass |
| 100 | +client.lapidary_deauthenticate('apiKeyAuth') |
114 | 101 | ```
|
115 |
| - |
116 |
| -In this example the method `client.private` can be called with the auth object returned by `client.login`. |
0 commit comments