Interaction & Next Steps¶
In the previous chapter, the security system—based on Lilya's dependency injection system was providing the path operation function
with a token
as a str
.
This token was extracted from the Authorization
header of the incoming request. The security system automatically handled this, so the function didn't need to worry about how the token was retrieved. The function simply received the token as a string, which it could then use for further processing, such as verifying the token's validity or checking user permissions.
from typing import Any
from lilya.apps import Lilya
from lilya.contrib.security.oauth2 import OAuth2PasswordBearer
from lilya.contrib.openapi.decorator import openapi
from lilya.dependencies import Provides, Provide
from lilya.routing import Path
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@openapi(security=[oauth2_scheme])
async def get_items(token: str = Provides()) -> dict[str, Any]:
return {"token": token}
app = Lilya(
routes=[
Path("/items", handler=get_items, dependencies={"token": Provide(oauth2_scheme)}),
]
)
That’s still not very useful as it is.
Let’s enhance it by returning the current user instead.
Create a user model¶
By creating a user
model you can use Pydantic
, msgspec or whatever you want since Lilya supports the encoders
making it versatile enough for your needs.
For ths example, let us use the native Pydantic support.
from lilya.apps import Lilya
from lilya.contrib.security.oauth2 import OAuth2PasswordBearer
from lilya.contrib.openapi.decorator import openapi
from lilya.dependencies import Security
from lilya.dependencies import Provides, Provide
from lilya.routing import Path
from pydantic import BaseModel
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str | None = None
def fake_decode_token(token):
return User(username=token + "fakedecoded", email="john@example.com")
async def get_current_user(token: str = Security(oauth2_scheme)):
user = fake_decode_token(token)
return user
@openapi(security=[oauth2_scheme])
async def users_me(current_user: User = Provides()) -> User:
return current_user
app = Lilya(
routes=[
Path(
"/users/me",
handler=users_me,
dependencies={"current_user": Provide(get_current_user)}
),
],
)
The get_current_user
dependency¶
Let's create a dependency called get_current_user
.
And remember, dependencies can have sub-dependencies, right?
from lilya.apps import Lilya
from lilya.contrib.security.oauth2 import OAuth2PasswordBearer
from lilya.contrib.openapi.decorator import openapi
from lilya.dependencies import Security
from lilya.dependencies import Provides, Provide
from lilya.routing import Path
from pydantic import BaseModel
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str | None = None
def fake_decode_token(token):
return User(username=token + "fakedecoded", email="john@example.com")
async def get_current_user(token: str = Security(oauth2_scheme)):
user = fake_decode_token(token)
return user
@openapi(security=[oauth2_scheme])
async def users_me(current_user: User = Provides()) -> User:
return current_user
app = Lilya(
routes=[
Path(
"/users/me",
handler=users_me,
dependencies={"current_user": Provide(get_current_user)}
),
],
)
The get_current_user
dependency will depend on the same oauth2_scheme
we created earlier.
Just like we did before in the path operation itself, our new get_current_user
dependency will receive a token
as a str
from the oauth2_scheme
sub-dependency.
Warning
You can see a Security
object there in the sub-dependency, right? Well, yes, that Security
object that depends
of the scheme
can only be called using this object.
In other words, when a sub-dependency is a oauth2_scheme
type of thing or any security related, you must use the Security
object.
This special object once its declared, Lilya will know what to do with it and make sure it can be executed properly.
Lilya dependency system is extremely powerful and extremely versatile and therefore some special objects dedicated to this security approach were added to make our lives simples.
Get the user¶
The get_current_user
dependency will use a (fake) utility function we created. This function takes the token as a str
and returns our Pydantic User
model.
from lilya.apps import Lilya
from lilya.contrib.security.oauth2 import OAuth2PasswordBearer
from lilya.contrib.openapi.decorator import openapi
from lilya.dependencies import Security
from lilya.dependencies import Provides, Provide
from lilya.routing import Path
from pydantic import BaseModel
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str | None = None
def fake_decode_token(token):
return User(username=token + "fakedecoded", email="john@example.com")
async def get_current_user(token: str = Security(oauth2_scheme)):
user = fake_decode_token(token)
return user
@openapi(security=[oauth2_scheme])
async def users_me(current_user: User = Provides()) -> User:
return current_user
app = Lilya(
routes=[
Path(
"/users/me",
handler=users_me,
dependencies={"current_user": Provide(get_current_user)}
),
],
)
Provide the current user¶
Now, we can use the Provide
and Provides
with our get_current_user
dependency in the path operation. This is part
of the special Esmerlad dependency inject system that is also multi layered. You can read again about the
dependency injection with Lilya.
from lilya.apps import Lilya
from lilya.contrib.security.oauth2 import OAuth2PasswordBearer
from lilya.contrib.openapi.decorator import openapi
from lilya.dependencies import Security
from lilya.dependencies import Provides, Provide
from lilya.routing import Path
from pydantic import BaseModel
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str | None = None
def fake_decode_token(token):
return User(username=token + "fakedecoded", email="john@example.com")
async def get_current_user(token: str = Security(oauth2_scheme)):
user = fake_decode_token(token)
return user
@openapi(security=[oauth2_scheme])
async def users_me(current_user: User = Provides()) -> User:
return current_user
app = Lilya(
routes=[
Path(
"/users/me",
handler=users_me,
dependencies={"current_user": Provide(get_current_user)}
),
],
)
Notice that we declare the type of current_user
as the Pydantic model User
.
This ensures that we get type checking and auto-completion support inside the function, making development smoother and more error-free.
Now, you can directly access the current user in the path operation functions and handle the security mechanisms at the Dependency Injection level, using Provides
.
You can use any model or data for your security requirements (in this case, a Pydantic model User
), but you're not limited to a specific data model, class, or type.
For example:
- Want to use an
id
andemail
instead of ausername
in your model? No problem, just use the same tools. - Prefer a
str
or adict
? Or perhaps a database class model instance directly? It all works seamlessly. - If you have bots, robots, or other systems logging in instead of users, and they only need an access token, that's fine too.
You can use any model, class, or database structure that fits your application's needs. Lilya's dependency injection system makes it easy and flexible for all cases.
Code size so far¶
This example might seem a bit verbose, but remember, we're combining security, data models, utility functions, and path operations in the same file.
Here’s the key takeaway:
The security and dependency injection setup is written once.
You can make it as complex as you need, but it only needs to be defined in one place. The beauty of Lilya is its flexibility—whether simple or complex, you only write this logic once.
And once it's set up, you can reuse it across thousands of endpoints (path operations).
All of these endpoints (or any portion of them) can take advantage of the same dependencies or any others you create.
Even with thousands of path operations, many of them can be as simple as just a few lines of code.
from lilya.apps import Lilya
from lilya.contrib.security.oauth2 import OAuth2PasswordBearer
from lilya.contrib.openapi.decorator import openapi
from lilya.dependencies import Security
from lilya.dependencies import Provides, Provide
from lilya.routing import Path
from pydantic import BaseModel
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str | None = None
def fake_decode_token(token):
return User(username=token + "fakedecoded", email="john@example.com")
async def get_current_user(token: str = Security(oauth2_scheme)):
user = fake_decode_token(token)
return user
@openapi(security=[oauth2_scheme])
async def users_me(current_user: User = Provides()) -> User:
return current_user
app = Lilya(
routes=[
Path(
"/users/me",
handler=users_me,
dependencies={"current_user": Provide(get_current_user)}
),
],
)
Remember that Lilya has a flexible dependency injection system and the lines can be cut by a lot avoiding repetition.
You can now access the current user directly in your path operation function.
We're already halfway there.
Next, we just need to add a path operation that allows the user/client to send their username
and password
to get the token. That will be our next step.