File dir
fastapi_jwt
.env
main.py
app
api.py
model.py
auth
auth_bearer.py
auth_handler.py
fastapi_jwt/.env
secret=please_please_update_me_please
algorithm=HS256
fastapi_jwt/main.py
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.api:app", host="127.0.0.1", port=8000, reload=True)
fastapi_jwt/app/api.py
from fastapi import FastAPI, Body, Depends
from starlette.responses import HTMLResponse
from app.model import PostSchema, UserSchema, UserLoginSchema
from app.auth.auth_bearer import JWTBearer
from app.auth.auth_handler import signJWT
app = FastAPI()
#app.get("/", tags=["root"])
async def read_root():
return {"message": "Welcome to your blog!."}
from app.model import PostSchema
posts = [
{
"id": 1,
"title": "Pancake",
"content": "Lorem Ipsum ..."
}
]
users = []
#app.get("/posts", tags=["posts"])
async def get_posts() -> dict:
return { "data": posts }
#app.get("/posts/{id}", tags=["posts"])
async def get_single_post(id: int) -> dict:
if id > len(posts):
return {
"error": "No such post with the supplied ID."
}
for post in posts:
if post["id"] == id:
return {
"data": post
}
#app.get("/post", dependencies=[Depends(JWTBearer())], tags=["posts"])
#app.post("/post", dependencies=[Depends(JWTBearer())], tags=["posts"])
async def add_post(post: PostSchema) -> dict:
post.id = len(posts) + 1
posts.append(post.dict())
return {"result":"Post added successfully"}
#app.post("/user/signup", tags=["user"])
async def create_user(user: UserSchema = Body(...)):
users.append(user) # replace with db call, making sure to hash the password first
return signJWT(user.email)
def check_user(data: UserLoginSchema):
for user in users:
if user.email == data.email and user.password == data.password:
return True
return False
#app.post("/user/login", tags=["user"])
async def user_login(user: UserLoginSchema = Body(...)):
if check_user(user):
return signJWT(user.email)
return {
"error": "Wrong login details!"
}
fastapi_jwt/app/model.py
from pydantic import BaseModel, Field, EmailStr
class PostSchema(BaseModel):
id: int = Field(default=None)
title: str = Field(...)
content: str = Field(...)
class Config:
schema_extra = {
"example": {
"title": "Securing FastAPI applications with JWT.",
"content": "In this tutorial, you'll learn how to secure your application by enabling authentication using JWT. We'll be using PyJWT to sign, encode and decode JWT tokens...."
}
}
class UserSchema(BaseModel):
fullname: str = Field(...)
email: EmailStr = Field(...)
password: str = Field(...)
class Config:
schema_extra = {
"example": {
"fullname": "Abdulazeez Abdulazeez Adeshina",
"email": "abdulazeez#x.com",
"password": "weakpassword"
}
}
class UserLoginSchema(BaseModel):
email: EmailStr = Field(...)
password: str = Field(...)
class Config:
schema_extra = {
"example": {
"email": "abdulazeez#x.com",
"password": "weakpassword"
}
}
fastapi_jwt/app/auth/auth_bearer.py
from fastapi import Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .auth_handler import decodeJWT
class JWTBearer(HTTPBearer):
def __init__(self, auto_error: bool = True):
super(JWTBearer, self).__init__(auto_error=auto_error)
async def __call__(self, request: Request):
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(status_code=403, detail="Invalid authentication scheme.")
if not self.verify_jwt(credentials.credentials):
raise HTTPException(status_code=403, detail="Invalid token or expired token.")
return credentials.credentials
else:
raise HTTPException(status_code=403, detail="Invalid authorization code.")
def verify_jwt(self, jwtoken: str) -> bool:
isTokenValid: bool = False
try:
payload = decodeJWT(jwtoken)
except:
payload = None
if payload:
isTokenValid = True
return isTokenValid
fastapi_jwt/app/auth/auth_handler.py
import time
from typing import Dict
import jwt
from decouple import config
JWT_SECRET = config("secret")
JWT_ALGORITHM = config("algorithm")
def token_response(token: str):
return {
"access_token": token
}
def signJWT(user_id: str) -> Dict[str, str]:
payload = {
"user_id": user_id,
"expires": time.time() + 600
}
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return token_response(token)
def decodeJWT(token: str) -> dict:
try:
decoded_token = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return decoded_token if decoded_token["expires"] >= time.time() else None
except:
return {}
I run 'main.py', the url
" http://127.0.0.1:8000/post " shows the output "{"detail":"Not authenticated"}".
So go to the url " http://127.0.0.1:8000/docs " for FastAPI -Swagger UI interface like
this.
Then I signup and login to get the jwt token and use that token to authenticate, then go to url
" /post " in FastAPI -Swagger UI and execute to get the output {"result": "Post added successfully"}.
But in " http://127.0.0.1:8000/post " got an error and show the same ouput "{"detail":"Not authenticated"}"
Related
I found an example from this web site,
https://blog.hanchon.live/guides/google-login-with-fastapi/
is there a similar example for microsoft AD authentication?
import os
import starlette
from starlette.config import Config
from authlib.integrations.starlette_client import OAuth
# OAuth settings
GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID') or None
GOOGLE_CLIENT_SECRET = os.environ.get('GOOGLE_CLIENT_SECRET') or None
if GOOGLE_CLIENT_SECRET is None or GOOGLE_CLIENT_ID is None:
raise BaseException('Missing env variables')
# setup OAuth
config_data = {'GOOGLE_CLIENT_ID': GOOGLE_CLIENT_ID,
'GOOGLE_CLIENT_SECRET': GOOGLE_CLIENT_SECRET}
starlette_config = Config(environ=config_data)
oauth = OAuth(starlette_config)
oauth.register(
name='google',
server_metada_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)
The output is:]On instance['id']:
'10'
Given JSON data is InValid
Traceback (most recent call last):
File "sample3.py", line 38, in
invalidjsonData = json.loads(
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/json/init.py", line 357, in loads
return _default_decoder.decode(s)
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/json/decoder.py", line 353, in raw_decode]
user_schema is:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "User",
"description": "A user request json",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for a user",
"type": "integer"
},
"name": {
"description": "Name of the user",
"type": "string"
},
"contact_number": {
"type": "number"
}
},
"required": ["id", "name", "contact_number"]
}
an example from github:
import json
from fastapi import FastAPI
from starlette.config import Config
from starlette.requests import Request
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, RedirectResponse
from authlib.integrations.starlette_client import OAuth, OAuthError
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="!secret")
config = Config('.env')
oauth = OAuth(config)
CONF_URL = 'https://accounts.google.com/.well-known/openid-configuration'
oauth.register(
name='google',
server_metadata_url=CONF_URL,
client_kwargs={
'scope': 'openid email profile'
}
)
#app.get('/')
async def homepage(request: Request):
user = request.session.get('user')
if user:
data = json.dumps(user)
html = (
f'<pre>{data}</pre>'
'logout'
)
return HTMLResponse(html)
return HTMLResponse('login')
#app.get('/login')
async def login(request: Request):
redirect_uri = request.url_for('auth')
return await oauth.google.authorize_redirect(request, redirect_uri)
#app.get('/auth')
async def auth(request: Request):
try:
token = await oauth.google.authorize_access_token(request)
except OAuthError as error:
return HTMLResponse(f'<h1>{error.error}</h1>')
user = token.get('userinfo')
if user:
request.session['user'] = dict(user)
return RedirectResponse(url='/')
#app.get('/logout')
async def logout(request: Request):
request.session.pop('user', None)
return RedirectResponse(url='/')
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host='127.0.0.1', port=8000)
I'm using BasePermission decorator as specified in documentation.
#strawberry.type
class Query:
#strawberry.field(permission_classes=[IsAuthenticated])
def user(self) -> User:
# get by token OFC
return User(user_id=1, email="vladimir#cw.tech", first_name = "Vladimir", last_name = "Kirilov")
In my impementation I use VerifyToken class as described in FastAPI auth0 documentation.
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
print(source)
print(info)
token: str = Depends(token_auth_scheme)
print(token)
result = VerifyToken(token.credentials).verify()
if result.get("status"):
print(result)
return False
return True
So I'm trying to get and verify the BEARER from the request, but I'm not able to extract it to process it further and get the error, please advise.
{
"data": null,
"errors": [
{
"message": "'Depends' object has no attribute 'credentials'",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"user"
]
}
]
}
Figured it out, not the cleanest way, but here it is
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
async def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
request: Union[Request, WebSocket] = info.context["request"]
print(request.headers)
if "Authorization" in request.headers:
print(request.headers['Authorization'])
result = VerifyToken( request.headers['Authorization'][7:] ).verify()
if result.get("status") == "error":
print(result.get("msg"))
return False
if result.get("sub"):
print(result)
return True
return False
In the FastAPI framework, the pydantic error messages are showing like below.
{"detail": [
{
"loc": [
"body",
"location",
"name"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"location",
"name12"
],
"msg": "extra fields not permitted",
"type": "value_error.extra"
}
]
}
I want to send a simple message: {"field-name":"error message"}.
In Pydantic document they mentioned like, create a model instance in the try: except blocks and construct the error message in the except block. But in fast API, model instance created by fastapi itself, for example, if I write an URL like below
#router.post("/", response_model=DataModelOut)
async def create_location(location: schemas.LocationIn, user: str = Depends(get_current_user) ):
return model.save(location,user)
Here the location instance created by fastapi itself is the problem.
Is there any way to construct the error message?
Actually this error messages coming from fastapi.exceptions, You can achieve that by overriding the custom exceptions,
Imagine i have simple app like this:
from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel
#app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"detail": exc.errors(),
"body": exc.body,
"your_additional_errors": {"Will be": "Inside", "This":" Error message"}}),
)
class Item(BaseModel):
title: str
size: int
#app.post("/items/")
async def create_item(item: Item):
return item
If i send values invalid values to my Request body
{
"title": 22,
"size": "hehe"
}
Now the error will be more customized:
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": 22,
"size": "hehe"
},
"your_additional_errors": {
"Will be": "Inside the",
"Error": "Message"
}
}
You can change the content of exception, everything is up to you.
I am writing a middle ware for it.
async def genrange(s):
import json
s = json.loads(s)
yield json.dumps({"message":{k.get("loc")[-1]:k.get("msg") for k in s['detail']},
"id":None})
#app.middleware("http")
async def add_process_time_header(request: Request, call_next):
response = await call_next(request)
status_code = response.status_code
if status_code >=300:
async for i in response.body_iterator:
data = genrange(i)
response.body_iterator = data
return response
I'm not sure you'll like my answer better than the other answers. You can create a custom pydantic, with diffrent error messages. Kinda overkill, but could solve you a specific problem. In this example, I am changing the error message, when I insert an unpermitted HttpUrlSceme.
class UrlSchemePermittedError(errors.UrlError):
code = 'url.scheme'
msg_template = 'URL scheme not cool'
def __init__(self, allowed_schemes: Set[str]):
super().__init__(allowed_schemes=allowed_schemes)
class AnyHttpUrlDirrentMessage(AnyUrl):
allowed_schemes = {'http', 'https'}
#classmethod
def validate_parts(cls, parts: Dict[str, str]) -> Dict[str, str]:
"""
A method used to validate parts of an URL.
Could be overridden to set default values for parts if missing
"""
scheme = parts['scheme']
if scheme is None:
raise errors.UrlSchemeError()
if cls.allowed_schemes and scheme.lower() not in cls.allowed_schemes:
raise UrlSchemePermittedError(cls.allowed_schemes)
port = parts['port']
if port is not None and int(port) > 65_535:
raise errors.UrlPortError()
user = parts['user']
if cls.user_required and user is None:
raise errors.UrlUserInfoError()
return parts
I have the following code which creates a yaml file from dictionary:
import yaml
from collections import OrderedDict
import json
from pprint import pprint
import random
import string
data = {
"test_name" : "Create_user test",
"stages":{
"name" : "check user sucessfully added",
"request": {
"url":"self.url",
"json":{
"username":"self.username",
"role":"self.role",
"userProfile":"self.userProfile"
},
"method":"self.method",
"headers":{"content_type":"application/json"}
},
"response" : {"status_code":200}
}
}
print(data)
def setup_yaml():
""" https://stackoverflow.com/a/8661021 """
represent_dict_order = lambda self, data: self.represent_mapping('tag:yaml.org,2002:map', data.items())
yaml.add_representer(OrderedDict, represent_dict_order)
setup_yaml()
with open('abc_try.tavern.yml', 'w') as outfile:
yaml.dump(OrderedDict(data), outfile, default_flow_style=False)
And I get the 'abc_try.tavern.yml' file as:
test_name: Create_user test
stages:
name: check user sucessfully added
request:
headers:
content_type: application/json
json:
role: self.role
userProfile: self.userProfile
username: self.username
method: self.method
url: self.url
response:
status_code: 200
but the I want the following file to be generated:
test_name: Create_user test
stages:
- name: check user sucessfully added
request:
headers:
content_type: application/json
json:
role: self.role
userProfile: self.userProfile
username: self.username
method: self.method
url: self.url
response:
status_code: 200
How to add the '-' in third line before 'name'.(Kindly mind the spacing and formatting of '-', i.e. just below 's' of 'stages'.
The '-' indicates a list element. So you have to put the inner dict in a list:
data = {
"test_name" : "Create_user test",
"stages": [
{
"name" : "check user sucessfully added",
# ...
}
]
}
I am using ODOO 11.0
how to return simple JSON object without JSON-RPC additional parameters
Here is my odoo controller code:
#http.route('/userappoint/webhook_test/',type='json', auth='public',method=['POST'], csrf=False,website=True)
def webhook_test(self,**kw):
response = {
'speech' : 'hello my name is shubham',
'displayText' : 'hello testing',
'source' : 'webhook'
}
return response
And I am getting this result :
{
"result": {
"displayText": "hello testing",
"source": "webhook",
"speech": "hello my name is shubham"
},
"id": "6eaced3e-6b0d-4518-9710-de91eaf16dd9",
"jsonrpc": "2.0"
}
But I need this :
{
"speech": "hello my name is shubham",
"displayText": "hello testing",
"source": "webhook"
}
Any help to point me in the right direction?
Thanks
Works on Odoo11. Just edit below portion of the function _json_response defined at odoo/odoo11/odoo/http.py near about line no : 621-630 as below & Restart the odoo service.
def _json_response(self, result=None, error=None):
response = {
'jsonrpc': '2.0',
'id': self.jsonrequest.get('id')
}
if error is not None:
response['error'] = error
if result is not None:
response['result'] = result
to new:
def _json_response(self, result=None, error=None):
response = {}
if error is not None:
response = error
if result is not None:
response = result
Then, restart the odoo service
Place the following code in any of your controller before you initialize the controller class
from odoo import http
from odoo.http import request, Response, JsonRequest
from odoo.tools import date_utils
class JsonRequestNew(JsonRequest):
def _json_response(self, result=None, error=None):
# response = {
# 'jsonrpc': '2.0',
# 'id': self.jsonrequest.get('id')
# }
# if error is not None:
# response['error'] = error
# if result is not None:
# response['result'] = result
responseData = super(JsonRequestNew, self)._json_response(result=result,error=error)
response = {}
if error is not None:
response = error
if result is not None:
response = result
mime = 'application/json'
body = json.dumps(response, default=date_utils.json_default)
return Response(
body, status=error and error.pop('http_status', 200) or 200,
headers=[('Content-Type', mime), ('Content-Length', len(body))]
)
class RootNew(http.Root):
def get_request(self, httprequest):
# deduce type of request
jsonResponse = super(RootNew, self).get_request(httprequest=httprequest)
if httprequest.mimetype in ("application/json", "application/json-rpc"):
return JsonRequestNew(httprequest)
else:
return jsonResponse
http.root = RootNew()
class MyController(http.Controller):