Skip to content

Commit

Permalink
update on documentation and ready for websocket
Browse files Browse the repository at this point in the history
  • Loading branch information
user committed Aug 23, 2022
1 parent 1354bd7 commit cf7b3d7
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 70 deletions.
3 changes: 2 additions & 1 deletion docs/advance/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ from .model import User


def admin_only(func: typing.Callable) -> typing.Callable:
idx = is_route_function(func)
# HTTP only
idx = is_route_function(func, "request")

if asyncio.iscoroutinefunction(func):
@functools.wraps(func)
Expand Down
83 changes: 77 additions & 6 deletions docs/custom/login-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

`LoginManager` is where we set the configuration for authentication.

Upon creating the instance, `redirect_to` property must be provided.
This property is url name of __redirect route__ that authentication is
required error redirected to.
When creating the instance, `redirect_to` and `secret_key` argument must be provided.
This `redirect_to` parameter is url name of __redirect route__ that authentication is
required error will be redirected to.

`redirect_to` value is either:

Expand All @@ -14,12 +14,83 @@ required error redirected to.
```python
from starlette_login.login_manager import LoginManager

login_manager = LoginManager(redirect_to='login', secret_key='yoursecretkey')
login_manager = LoginManager(redirect_to='login', secret_key='secretkey')
```

The `login_manager` instance is going to be passed to **Starlette** application **state**, authentication __Backend__ and __Middleware__.
The `login_manager` instance is going to be passed to **Starlette**
application **state**, authentication __Backend__ and __Middleware__.


## Protection Level

There are 2 protection level `Basic` (_default_) and `Strong`.

Protection level will affect session and cookie session
when __session identifier__ (hash of user-agent and IP address) changed.

When the session is permanent and __protection level__ is `Strong`,
then the session will simply be marked as non-fresh,
and anything requiring a fresh login will force the user
to re-authenticate while `Basic` will only mark
the current session as non-`fresh`.

If the identifiers do not match in `Strong` mode for a non-permanent session,
then the entire session (as well as the remember-token if it exists) is deleted.

__Usage__

```python
from starlette_login.login_manager import Config, LoginManager, ProtectionLevel

config = Config(protection_level=ProtectionLevel.Strong)
login_manager = LoginManager(
redirect_to='login', secret_key='secretkey', config=config
)
```


## User Loader Callback

You need to set up `user loader callback` function to load `user` for authentication session.

__Callback signature__

```python
import typing
from starlette.requests import Request

# async def / def
async def load_user(request: Request, user_id: typing.Any):
...
return user
```

__Usage__

```python
login_manager.set_user_loader(load_user)
```

## Websocket Authentication Error Callback

If you need to send custom message on `ws_login_required` decorated router,
You can call `login_manager.set_ws_not_authenticated` method.

By default, authentication error will __close__ the websocket connection.

__Usage__

```python
from starlette.websockets import WebSocket

async def custom_ws_auth_error(websocket: WebSocket):
await websocket.send_text('not authenticated')
await websocket.close()


login_manager.set_ws_not_authenticated(custom_ws_auth_error)
```

## Config

You can pass custom configuration to `LoginManager` instance to set custom `cookie` values such as:
Expand All @@ -33,4 +104,4 @@ You can pass custom configuration to `LoginManager` instance to set custom `cook
- COOKIE_SAMESITE


See [Configuration](/advance/configuration) section for more information.
See [Configuration](./configuration.md) section for more information.
10 changes: 10 additions & 0 deletions docs/custom/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Excluded Path

### Note

`request.user` call will lead to
`AssertionError: AuthenticationMiddleware must be installed to access request.user`.
Our authentication middleware is ignoring the excluded path
and `request.user` is not being set.
The exception raised by `starlette`.
You do not need to use `AuthenticationMiddleware` except you need to.
16 changes: 8 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ Development
pip install 'git+https://github.com/jockerz/Starlette-Login'
```

## Usage Examples

- [Basic Auth](https://github.com/jockerz/Starlette-Login-Example/tree/main/basic_auth)
- Token Auth: *TODO*
- Multiple Auth: *TODO*


## Usage

Expand Down Expand Up @@ -199,7 +193,7 @@ from .model import user_list
from .routes import home_page, login_page, logout_page, protected_page


login_manager = LoginManager(redirect_to='login')
login_manager = LoginManager(redirect_to='login', secret_key='secret')
login_manager.set_user_loader(user_list.user_loader)

app = Starlette(
Expand All @@ -210,7 +204,6 @@ app = Starlette(
backend=SessionAuthBackend(login_manager),
login_manager=login_manager,
login_route='login',
secret_key='secret',
)
],

Expand All @@ -230,4 +223,11 @@ app.state.login_manager = login_manager
uvicorn app:app
```

## More Examples

- [Basic Auth](https://github.com/jockerz/Starlette-Login-Example/tree/main/basic_auth)
- Token Auth: *TODO*
- Multiple Auth: *TODO*


[Flask-Login]: https://flask-login.readthedocs.io
10 changes: 0 additions & 10 deletions docs/middleware.md

This file was deleted.

3 changes: 1 addition & 2 deletions docs/tutorial/sqladmin.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,8 @@ middleware = [
AuthenticationMiddleware,
backend=SessionAuthBackend(login_manager),
login_manager=login_manager,
secret_key=SECRET_KEY,
excluded_dirs=['/static']
)
),
)
```

Expand Down
118 changes: 118 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## Login Manager

First of all, you need to create a `LoginManager` instance.
The login manager manage the `Starlette-Login` behaviour of your `Starllate` instance.

```python
from starlette_login.login_manager import LoginManager

login_manager = LoginManager(redirect_to='/login_endpoint', secret_key=SECRET_KEY)
```

Then you will need to provide a __user loader__ callback function.

```python
from starlette.requests import Request

from model import User

async def get_user_by_id(request: Request, user_id: int):
# return a sub class of `mixin.UserMixin` instance
db = request.state.db
user = await User.get_by_id(db, user_id)
return user


login_manager.set_user_loader(get_user_by_id)
```

## User Class

User class mush inherit `UserMixin` class.

```python
from starlette_login.mixins import UserMixin

class User(UserMixin):
user_id: int
name: str

def identity(self) -> int:
return self.user_id

def display_name(self):
return self.name
```

## Starlette Application and Middleware

Upon creation of `Starlette` instance, we add `SessionMiddleware` and `AuthenticationMiddleware`.

`SessionMiddleware` is required to manage `http` and `websocket` session.

```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette_login.backends import SessionAuthBackend
from starlette_login.middleware import AuthenticationMiddleware

app = Starlette(
middleware=[
Middleware(SessionMiddleware, secret_key=SECRET_KEY),
Middleware(
AuthenticationMiddleware,
backend=SessionAuthBackend(login_manager),
login_manager=login_manager,
login_route='login',
)
],
...
)
```

Then you need to add the `login manager` to `Starlette` instance `state`.

```python
app.state.login_manager = login_manager
```

## Login and Logout

Now that the `Starlette` application instance is ready to use,
you will need to create a `login` and `logout` route to manage user authentication.

See `routes.py` on [Basic](index.md) page for `login` and `logout` route example.


## Decorator

`Starlette-Login` Decorator helps to prevent non-authorized user to access certain route.
There are 3 available __decorator__:

- `login_required`: only authenticated user can access the page
- `fresh_login_required`: only _newly logged-in_ user can access the page
- `ws_login_required`: websocket route version of `login_required`

__Usage__

```python
from starlette.requests import Request
from starlette.responses import PlainTextResponse
from starlette.websockets import WebSocket
from starlette_login.decorator import login_required, ws_login_required


@login_required
async def protected_page(request: Request):
return PlainTextResponse(f'You are logged in as {request.user.username}')


@ws_login_required
async def ws_endpoint(websocket: WebSocket):
await websocket.accept()
await websocket.send_text("authenticated")
await websocket.close()
```

See [tests/views.py](https://github.com/jockerz/Starlette-Login/blob/main/tests/views.py) for more decorated routes example.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ edit_uri: ""

nav:
- Basic: index.md
- Usage: usage.md
- Customization:
- Login Manager: custom/login-manager.md
- Configuration: custom/configuration.md
Expand Down
2 changes: 1 addition & 1 deletion starlette_login/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.4"
__version__ = "0.1.5"
35 changes: 18 additions & 17 deletions starlette_login/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .login_manager import LoginManager
from .mixins import UserMixin
from .utils import create_identifier


class BaseAuthenticationBackend:
Expand All @@ -22,28 +23,28 @@ async def authenticate(
self, conn: HTTPConnection
) -> t.Optional[UserMixin]:
# Load user from session
session_key = self.login_manager.config.SESSION_NAME_KEY
user_id = conn.session.get(session_key)

remember_cookie = self.login_manager.config.REMEMBER_COOKIE_NAME
session_fresh = self.login_manager.config.SESSION_NAME_FRESH

# Using Strong protection
if self.login_manager.protection_is_strong():
for key in self.login_manager.config.session_keys:
try:
conn.session.pop(key)
except KeyError:
pass
conn.session[remember_cookie] = "clear"
else:
config = self.login_manager.config
remember_cookie = config.REMEMBER_COOKIE_NAME
session_fresh = config.SESSION_NAME_FRESH

session = conn.session.get(config.SESSION_NAME_ID)
identifier = create_identifier(conn)

if identifier != session:
if self.login_manager.protection_is_strong():
# Strong protection
for key in config.session_keys:
conn.session.pop(key, None)
# conn.session[remember_cookie] = "clear"
# else:
conn.session[session_fresh] = False
user_id = conn.session.get(config.SESSION_NAME_KEY)

if (
user_id is None
and conn.session.get(remember_cookie, "clear") != "clear"
and conn.session.get(remember_cookie) != "clear"
):
cookie = conn.cookies.get(self.login_manager.config.COOKIE_NAME)
cookie = conn.cookies.get(config.COOKIE_NAME)
if cookie:
user_id = self.login_manager.get_cookie(cookie)
user_id = int(user_id)
Expand Down
2 changes: 1 addition & 1 deletion starlette_login/login_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class Config:
SESSION_NAME_ID: str = "_id"
SESSION_NAME_KEY: str = "_user_id"
SESSION_NAME_NEXT: str = "next"

REMEMBER_COOKIE_NAME: str = "_remember"
REMEMBER_SECONDS_NAME: str = "_remember_seconds"
EXEMPT_METHODS: t.Tuple[str] = ("OPTIONS",)
Expand All @@ -39,6 +38,7 @@ class Config:
COOKIE_PATH: str = "/"
COOKIE_SECURE: bool = False
COOKIE_HTTPONLY: bool = True
# Not supported on python 3.6 and 3.7
# COOKIE_SAMESITE: t.Optional[t.Literal["lax", "strict", "none"]] = None
COOKIE_SAMESITE: t.Optional[str] = None
COOKIE_DURATION: timedelta = timedelta(days=365)
Expand Down

0 comments on commit cf7b3d7

Please sign in to comment.