Skip to content

Commit 8ed76da

Browse files
committed
add docs websocket protecting
1 parent cd4cdaa commit 8ed76da

File tree

6 files changed

+266
-6
lines changed

6 files changed

+266
-6
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.5.0
2+
* Support for WebSocket authorization *(Thanks to @SelfhostedPro for make issues)*
3+
* Function **get_raw_jwt()** can pass parameter encoded_token
4+
15
## 0.4.0
26
* Support set and unset cookies when returning a **Response** directly
37

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

2731
## 0.2.0
2832

docs/advanced-usage/websocket.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
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.
2+
3+
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.
4+
5+
**Note**: *Change all IP address to your localhost*
6+
7+
Here is an example of how you authorize from query URL:
8+
```python hl_lines="42-52 65-66 71 73"
9+
{!../examples/websocket.py!}
10+
```
11+
You will see a simple page like this:
12+
13+
<figure>
14+
<img src="https://bit.ly/3k2BpaM"/>
15+
</figure>
16+
17+
You can copy the token from endpoint **/login** and then send them:
18+
19+
<figure>
20+
<img src="https://bit.ly/3k4Y9XC"/>
21+
</figure>
22+
23+
And your WebSocket route will respond back if the token is valid or not:
24+
25+
<figure>
26+
<img src="https://bit.ly/36ajZ7d"/>
27+
</figure>
28+
29+
30+
Here is an example of how you authorize from cookie:
31+
```python hl_lines="30-47 60-61 66 68"
32+
{!../examples/websocket_cookie.py!}
33+
```
34+
35+
You will see a simple page like this:
36+
37+
<figure>
38+
<img src="https://bit.ly/2TXs8Gi"/>
39+
</figure>
40+
41+
You can get the token from URL **/get-cookie**:
42+
43+
<figure>
44+
<img src="https://bit.ly/2I9qtLG"/>
45+
</figure>
46+
47+
And click button send then your WebSocket route will respond back if the
48+
cookie and csrf token is match or cookie is valid or not:
49+
50+
<figure>
51+
<img src="https://bit.ly/3l3D8hB"/>
52+
</figure>

docs/api-doc.md

+45-5
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,62 @@ In here you will find the API for everything exposed in this extension.
1818

1919
### Protected Endpoint
2020

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

25-
**jwt_optional**()
25+
* Parameters:
26+
* **auth_from**: For identity get token from HTTP or WebSocket
27+
* **token**: The encoded JWT, it's required if the protected endpoint use WebSocket to
28+
authorization and get token from Query Url or Path
29+
* **websocket**: An instance of WebSocket, it's required if protected endpoint use a cookie to authorization
30+
* **csrf_token**: The CSRF double submit token. Since WebSocket cannot add specifying additional headers
31+
its must be passing csrf_token manually and can achieve by Query Url or Path
32+
* Returns: None
33+
34+
**jwt_optional**(auth_from="request", token=None, websocket=None, csrf_token=None)
2635
: *If an access token present in the request, this will call the endpoint with `get_jwt_identity()`
2736
having the identity of the access token. If no access token is present in the request, this endpoint
2837
will still be called, but `get_jwt_identity()` will return None instead.*
2938

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

33-
**jwt_refresh_token_required**()
42+
* Parameters:
43+
* **auth_from**: For identity get token from HTTP or WebSocket
44+
* **token**: The encoded JWT, it's required if the protected endpoint use WebSocket to
45+
authorization and get token from Query Url or Path
46+
* **websocket**: An instance of WebSocket, it's required if protected endpoint use a cookie to authorization
47+
* **csrf_token**: The CSRF double submit token. Since WebSocket cannot add specifying additional headers
48+
its must be passing csrf_token manually and can achieve by Query Url or Path
49+
* Returns: None
50+
51+
**jwt_refresh_token_required**(auth_from="request", token=None, websocket=None, csrf_token=None)
3452
: *If you call this function, it will ensure that the requester has a valid refresh token before
3553
executing the code below your router.*
3654

37-
**fresh_jwt_required**()
55+
* Parameters:
56+
* **auth_from**: For identity get token from HTTP or WebSocket
57+
* **token**: The encoded JWT, it's required if the protected endpoint use WebSocket to
58+
authorization and get token from Query Url or Path
59+
* **websocket**: An instance of WebSocket, it's required if protected endpoint use a cookie to authorization
60+
* **csrf_token**: The CSRF double submit token. Since WebSocket cannot add specifying additional headers
61+
its must be passing csrf_token manually and can achieve by Query Url or Path
62+
* Returns: None
63+
64+
**fresh_jwt_required**(auth_from="request", token=None, websocket=None, csrf_token=None)
3865
: *If you call this function, it will ensure that the requester has a valid and fresh access token before
3966
executing the code below your router.*
4067

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

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

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

149+
* Parameters:
150+
* **encoded_token**: The encoded JWT from parameter
151+
* Returns: Claims of JWT
152+
113153
**get_jti**(encoded_token)
114154
: *Returns the JTI (unique identifier) of an encoded JWT*
115155

examples/websocket.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from fastapi import FastAPI, WebSocket, Depends, Request, HTTPException, Query
2+
from fastapi.responses import HTMLResponse, JSONResponse
3+
from fastapi_jwt_auth import AuthJWT
4+
from fastapi_jwt_auth.exceptions import AuthJWTException
5+
from pydantic import BaseModel
6+
7+
app = FastAPI()
8+
9+
class User(BaseModel):
10+
username: str
11+
password: str
12+
13+
class Settings(BaseModel):
14+
authjwt_secret_key: str = "secret"
15+
16+
@AuthJWT.load_config
17+
def get_config():
18+
return Settings()
19+
20+
@app.exception_handler(AuthJWTException)
21+
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
22+
return JSONResponse(
23+
status_code=exc.status_code,
24+
content={"detail": exc.message}
25+
)
26+
27+
28+
html = """
29+
<!DOCTYPE html>
30+
<html>
31+
<head>
32+
<title>Authorize</title>
33+
</head>
34+
<body>
35+
<h1>WebSocket Authorize</h1>
36+
<p>Token:</p>
37+
<textarea id="token" rows="4" cols="50"></textarea><br><br>
38+
<button onclick="websocketfun()">Send</button>
39+
<ul id='messages'>
40+
</ul>
41+
<script>
42+
const websocketfun = () => {
43+
let token = document.getElementById("token").value
44+
let ws = new WebSocket(`ws://192.168.18.202:8000/ws?token=${token}`)
45+
ws.onmessage = (event) => {
46+
let messages = document.getElementById('messages')
47+
let message = document.createElement('li')
48+
let content = document.createTextNode(event.data)
49+
message.appendChild(content)
50+
messages.appendChild(message)
51+
}
52+
}
53+
</script>
54+
</body>
55+
</html>
56+
"""
57+
58+
@app.get("/")
59+
async def get():
60+
return HTMLResponse(html)
61+
62+
@app.websocket('/ws')
63+
async def websocket(websocket: WebSocket, token: str = Query(...), Authorize: AuthJWT = Depends()):
64+
await websocket.accept()
65+
try:
66+
Authorize.jwt_required("websocket",token=token)
67+
# Authorize.jwt_optional("websocket",token=token)
68+
# Authorize.jwt_refresh_token_required("websocket",token=token)
69+
# Authorize.fresh_jwt_required("websocket",token=token)
70+
await websocket.send_text("Successfully Login!")
71+
decoded_token = Authorize.get_raw_jwt(token)
72+
await websocket.send_text(f"Here your decoded token: {decoded_token}")
73+
except AuthJWTException as err:
74+
await websocket.send_text(err.message)
75+
await websocket.close()
76+
77+
@app.post('/login')
78+
def login(user: User, Authorize: AuthJWT = Depends()):
79+
if user.username != "test" or user.password != "test":
80+
raise HTTPException(status_code=401,detail="Bad username or password")
81+
82+
access_token = Authorize.create_access_token(subject=user.username,fresh=True)
83+
refresh_token = Authorize.create_refresh_token(subject=user.username)
84+
return {"access_token": access_token, "refresh_token": refresh_token}

examples/websocket_cookie.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from fastapi import FastAPI, WebSocket, Depends, Query
2+
from fastapi.responses import HTMLResponse
3+
from fastapi_jwt_auth import AuthJWT
4+
from fastapi_jwt_auth.exceptions import AuthJWTException
5+
from pydantic import BaseModel
6+
7+
app = FastAPI()
8+
9+
class Settings(BaseModel):
10+
authjwt_secret_key: str = "secret"
11+
authjwt_token_location: set = {"cookies"}
12+
13+
@AuthJWT.load_config
14+
def get_config():
15+
return Settings()
16+
17+
18+
html = """
19+
<!DOCTYPE html>
20+
<html>
21+
<head>
22+
<title>Authorize</title>
23+
</head>
24+
<body>
25+
<h1>WebSocket Authorize</h1>
26+
<button onclick="websocketfun()">Send</button>
27+
<ul id='messages'>
28+
</ul>
29+
<script>
30+
const getCookie = (name) => {
31+
const value = `; ${document.cookie}`;
32+
const parts = value.split(`; ${name}=`);
33+
if (parts.length === 2) return parts.pop().split(';').shift();
34+
}
35+
36+
const websocketfun = () => {
37+
let csrf_token = getCookie("csrf_access_token")
38+
39+
let ws = new WebSocket(`ws://192.168.18.202:8000/ws?csrf_token=${csrf_token}`)
40+
ws.onmessage = (event) => {
41+
let messages = document.getElementById('messages')
42+
let message = document.createElement('li')
43+
let content = document.createTextNode(event.data)
44+
message.appendChild(content)
45+
messages.appendChild(message)
46+
}
47+
}
48+
</script>
49+
</body>
50+
</html>
51+
"""
52+
53+
@app.get("/")
54+
async def get():
55+
return HTMLResponse(html)
56+
57+
@app.websocket('/ws')
58+
async def websocket(websocket: WebSocket, csrf_token: str = Query(...), Authorize: AuthJWT = Depends()):
59+
await websocket.accept()
60+
try:
61+
Authorize.jwt_required("websocket",websocket=websocket,csrf_token=csrf_token)
62+
# Authorize.jwt_optional("websocket",websocket=websocket,csrf_token=csrf_token)
63+
# Authorize.jwt_refresh_token_required("websocket",websocket=websocket,csrf_token=csrf_token)
64+
# Authorize.fresh_jwt_required("websocket",websocket=websocket,csrf_token=csrf_token)
65+
await websocket.send_text("Successfully Login!")
66+
decoded_token = Authorize.get_raw_jwt()
67+
await websocket.send_text(f"Here your decoded token: {decoded_token}")
68+
except AuthJWTException as err:
69+
await websocket.send_text(err.message)
70+
await websocket.close()
71+
72+
@app.get('/get-cookie')
73+
def get_cookie(Authorize: AuthJWT = Depends()):
74+
access_token = Authorize.create_access_token(subject='test',fresh=True)
75+
refresh_token = Authorize.create_refresh_token(subject='test')
76+
77+
Authorize.set_access_cookies(access_token)
78+
Authorize.set_refresh_cookies(refresh_token)
79+
return {"msg":"Successfully login"}

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ nav:
4343
- Asymmetric Algorithm: advanced-usage/asymmetric.md
4444
- Dynamic Token Expires: advanced-usage/dynamic-expires.md
4545
- Dynamic Token Algorithm: advanced-usage/dynamic-algorithm.md
46+
- WebSocket Protecting: advanced-usage/websocket.md
4647
- Bigger Applications: advanced-usage/bigger-app.md
4748
- Generate Documentation: advanced-usage/generate-docs.md
4849
- Configuration Options:

0 commit comments

Comments
 (0)