Skip to content

Commit

Permalink
Merge pull request #17 from IndominusByte/0.5.0-dev
Browse files Browse the repository at this point in the history
Version 0.5.0
  • Loading branch information
IndominusByte authored Nov 6, 2020
2 parents c4751fa + 8ed76da commit 532d44c
Show file tree
Hide file tree
Showing 15 changed files with 845 additions and 151 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
name: Build Docs

on: [push, pull_request]
on:
push:
branches:
- master
pull_request:
branches:
- master

jobs:
deploy:
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.5.0
* Support for WebSocket authorization *(Thanks to @SelfhostedPro for make issues)*
* Function **get_raw_jwt()** can pass parameter encoded_token

## 0.4.0
* Support set and unset cookies when returning a **Response** directly

Expand All @@ -22,7 +26,7 @@
* Custom error message key and status code
* JWT in cookies *(Thanks to @m4nuC for make issues)*
* Add Additional claims
* Add Documentation *(#9 by @paulussimanjuntak)*
* Add Documentation PR #9 by @paulussimanjuntak

## 0.2.0

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ FastAPI extension that provides JWT Auth support (secure, easy to use and lightw
- Access tokens and refresh tokens
- Freshness Tokens
- Revoking Tokens
- Support for WebSocket authorization
- Support for adding custom claims to JSON Web Tokens
- Storing tokens in cookies and CSRF protection

Expand Down
52 changes: 52 additions & 0 deletions docs/advanced-usage/websocket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
The WebSocket protocol doesn’t handle authorization or authentication. Practically, this means that a WebSocket opened from a page behind auth doesn’t "automatically" receive any sort of auth. You need to take steps to also secure the WebSocket connection.

Since you cannot customize WebSocket headers from JavaScript, you’re limited to the "implicit" auth (i.e. Basic or cookies) that’s sent from the browser. The more common approach to generates a token from your normal HTTP server and then have the client send the token (either as a query string in the WebSocket path or as the first WebSocket message). The WebSocket server then validates that the token is valid.

**Note**: *Change all IP address to your localhost*

Here is an example of how you authorize from query URL:
```python hl_lines="42-52 65-66 71 73"
{!../examples/websocket.py!}
```
You will see a simple page like this:

<figure>
<img src="https://bit.ly/3k2BpaM"/>
</figure>

You can copy the token from endpoint **/login** and then send them:

<figure>
<img src="https://bit.ly/3k4Y9XC"/>
</figure>

And your WebSocket route will respond back if the token is valid or not:

<figure>
<img src="https://bit.ly/36ajZ7d"/>
</figure>


Here is an example of how you authorize from cookie:
```python hl_lines="30-47 60-61 66 68"
{!../examples/websocket_cookie.py!}
```

You will see a simple page like this:

<figure>
<img src="https://bit.ly/2TXs8Gi"/>
</figure>

You can get the token from URL **/get-cookie**:

<figure>
<img src="https://bit.ly/2I9qtLG"/>
</figure>

And click button send then your WebSocket route will respond back if the
cookie and csrf token is match or cookie is valid or not:

<figure>
<img src="https://bit.ly/3l3D8hB"/>
</figure>
50 changes: 45 additions & 5 deletions docs/api-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,62 @@ In here you will find the API for everything exposed in this extension.

### Protected Endpoint

**jwt_required**()
**jwt_required**(auth_from="request", token=None, websocket=None, csrf_token=None)
: *If you call this function, it will ensure that the requester has a valid access token before
executing the code below your router. This does not check the freshness of the access token.*

**jwt_optional**()
* Parameters:
* **auth_from**: For identity get token from HTTP or WebSocket
* **token**: The encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
* **websocket**: An instance of WebSocket, it's required if protected endpoint use a cookie to authorization
* **csrf_token**: The CSRF double submit token. Since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
* Returns: None

**jwt_optional**(auth_from="request", token=None, websocket=None, csrf_token=None)
: *If an access token present in the request, this will call the endpoint with `get_jwt_identity()`
having the identity of the access token. If no access token is present in the request, this endpoint
will still be called, but `get_jwt_identity()` will return None instead.*

*If there is an invalid access token in the request (expired, tampered with, etc),
this will still call the appropriate error handler.*

**jwt_refresh_token_required**()
* Parameters:
* **auth_from**: For identity get token from HTTP or WebSocket
* **token**: The encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
* **websocket**: An instance of WebSocket, it's required if protected endpoint use a cookie to authorization
* **csrf_token**: The CSRF double submit token. Since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
* Returns: None

**jwt_refresh_token_required**(auth_from="request", token=None, websocket=None, csrf_token=None)
: *If you call this function, it will ensure that the requester has a valid refresh token before
executing the code below your router.*

**fresh_jwt_required**()
* Parameters:
* **auth_from**: For identity get token from HTTP or WebSocket
* **token**: The encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
* **websocket**: An instance of WebSocket, it's required if protected endpoint use a cookie to authorization
* **csrf_token**: The CSRF double submit token. Since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
* Returns: None

**fresh_jwt_required**(auth_from="request", token=None, websocket=None, csrf_token=None)
: *If you call this function, it will ensure that the requester has a valid and fresh access token before
executing the code below your router.*

* Parameters:
* **auth_from**: For identity get token from HTTP or WebSocket
* **token**: The encoded JWT, it's required if the protected endpoint use WebSocket to
authorization and get token from Query Url or Path
* **websocket**: An instance of WebSocket, it's required if protected endpoint use a cookie to authorization
* **csrf_token**: The CSRF double submit token. Since WebSocket cannot add specifying additional headers
its must be passing csrf_token manually and can achieve by Query Url or Path
* Returns: None

### Utilities

**create_access_token**(subject, fresh=False, algorithm=None, headers=None, expires_time=None, audience=None, user_claims={})
Expand Down Expand Up @@ -106,10 +142,14 @@ In here you will find the API for everything exposed in this extension.
* **response**: The FastAPI response object to delete the refresh cookies in
* Returns: None

**get_raw_jwt**()
**get_raw_jwt**(encoded_token=None)
: *This will return the python dictionary which has all of the claims of the JWT that is accessing the endpoint.
If no JWT is currently present, return `None` instead.*

* Parameters:
* **encoded_token**: The encoded JWT from parameter
* Returns: Claims of JWT

**get_jti**(encoded_token)
: *Returns the JTI (unique identifier) of an encoded JWT*

Expand Down
2 changes: 1 addition & 1 deletion examples/asymmetric.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic import BaseModel

# In the real case, you can put the
# public key and private key in *.txt then you can read that file
# public key and private key in *.pem, *.key then you can read that file
private_key = """
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgGBoQhqHdMU65aSBQVC/u9a6HMfKA927aZOk7HA/kXuA5UU4Sl+U
Expand Down
84 changes: 84 additions & 0 deletions examples/websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from fastapi import FastAPI, WebSocket, Depends, Request, HTTPException, Query
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi_jwt_auth import AuthJWT
from fastapi_jwt_auth.exceptions import AuthJWTException
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
username: str
password: str

class Settings(BaseModel):
authjwt_secret_key: str = "secret"

@AuthJWT.load_config
def get_config():
return Settings()

@app.exception_handler(AuthJWTException)
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.message}
)


html = """
<!DOCTYPE html>
<html>
<head>
<title>Authorize</title>
</head>
<body>
<h1>WebSocket Authorize</h1>
<p>Token:</p>
<textarea id="token" rows="4" cols="50"></textarea><br><br>
<button onclick="websocketfun()">Send</button>
<ul id='messages'>
</ul>
<script>
const websocketfun = () => {
let token = document.getElementById("token").value
let ws = new WebSocket(`ws://192.168.18.202:8000/ws?token=${token}`)
ws.onmessage = (event) => {
let messages = document.getElementById('messages')
let message = document.createElement('li')
let content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
}
}
</script>
</body>
</html>
"""

@app.get("/")
async def get():
return HTMLResponse(html)

@app.websocket('/ws')
async def websocket(websocket: WebSocket, token: str = Query(...), Authorize: AuthJWT = Depends()):
await websocket.accept()
try:
Authorize.jwt_required("websocket",token=token)
# Authorize.jwt_optional("websocket",token=token)
# Authorize.jwt_refresh_token_required("websocket",token=token)
# Authorize.fresh_jwt_required("websocket",token=token)
await websocket.send_text("Successfully Login!")
decoded_token = Authorize.get_raw_jwt(token)
await websocket.send_text(f"Here your decoded token: {decoded_token}")
except AuthJWTException as err:
await websocket.send_text(err.message)
await websocket.close()

@app.post('/login')
def login(user: User, Authorize: AuthJWT = Depends()):
if user.username != "test" or user.password != "test":
raise HTTPException(status_code=401,detail="Bad username or password")

access_token = Authorize.create_access_token(subject=user.username,fresh=True)
refresh_token = Authorize.create_refresh_token(subject=user.username)
return {"access_token": access_token, "refresh_token": refresh_token}
79 changes: 79 additions & 0 deletions examples/websocket_cookie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from fastapi import FastAPI, WebSocket, Depends, Query
from fastapi.responses import HTMLResponse
from fastapi_jwt_auth import AuthJWT
from fastapi_jwt_auth.exceptions import AuthJWTException
from pydantic import BaseModel

app = FastAPI()

class Settings(BaseModel):
authjwt_secret_key: str = "secret"
authjwt_token_location: set = {"cookies"}

@AuthJWT.load_config
def get_config():
return Settings()


html = """
<!DOCTYPE html>
<html>
<head>
<title>Authorize</title>
</head>
<body>
<h1>WebSocket Authorize</h1>
<button onclick="websocketfun()">Send</button>
<ul id='messages'>
</ul>
<script>
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
const websocketfun = () => {
let csrf_token = getCookie("csrf_access_token")
let ws = new WebSocket(`ws://192.168.18.202:8000/ws?csrf_token=${csrf_token}`)
ws.onmessage = (event) => {
let messages = document.getElementById('messages')
let message = document.createElement('li')
let content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
}
}
</script>
</body>
</html>
"""

@app.get("/")
async def get():
return HTMLResponse(html)

@app.websocket('/ws')
async def websocket(websocket: WebSocket, csrf_token: str = Query(...), Authorize: AuthJWT = Depends()):
await websocket.accept()
try:
Authorize.jwt_required("websocket",websocket=websocket,csrf_token=csrf_token)
# Authorize.jwt_optional("websocket",websocket=websocket,csrf_token=csrf_token)
# Authorize.jwt_refresh_token_required("websocket",websocket=websocket,csrf_token=csrf_token)
# Authorize.fresh_jwt_required("websocket",websocket=websocket,csrf_token=csrf_token)
await websocket.send_text("Successfully Login!")
decoded_token = Authorize.get_raw_jwt()
await websocket.send_text(f"Here your decoded token: {decoded_token}")
except AuthJWTException as err:
await websocket.send_text(err.message)
await websocket.close()

@app.get('/get-cookie')
def get_cookie(Authorize: AuthJWT = Depends()):
access_token = Authorize.create_access_token(subject='test',fresh=True)
refresh_token = Authorize.create_refresh_token(subject='test')

Authorize.set_access_cookies(access_token)
Authorize.set_refresh_cookies(refresh_token)
return {"msg":"Successfully login"}
2 changes: 0 additions & 2 deletions fastapi_jwt_auth/auth_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
class AuthConfig:
_token = None
_token_location = {'headers'}
_response = None
_request = None

_secret_key = None
_public_key = None
Expand Down
Loading

0 comments on commit 532d44c

Please sign in to comment.