Tert0 / fastapi-discord

Discord OAuth FastAPI extension for APIs
MIT License
71 stars 16 forks source link

Getting back only code but no access token, or refresh token. #96

Open javierohern opened 1 year ago

javierohern commented 1 year ago

Title. Discord developer portal knows about my redirect_uri which is http://localhost:8000/callback

`discord = DiscordOAuthClient(
    "omitted client id",
    "omitted secret",
    "http://localhost:8000/callback",
    (
        "identify",
        "guilds",
        "email"
     )
)

@app.get("/login")
async def login():
    return {"url": discord.oauth_login_url}

@app.get("/callback")
async def callback(code: str):
    print(f"Received code: {code}")
    tokens = await discord.get_access_token(code)
    access_token = tokens[0]
    refresh_token = tokens[1]
    print(f"Access Token: {access_token}")
    print(f"Refresh Token: {refresh_token}")
    return {"access_token": access_token, "refresh_token": refresh_token}

@app.get(
    "/authenticated",
    dependencies=[Depends(discord.requires_authorization)],
    response_model=bool,
)
async def isAuthenticated(token: str = Depends(discord.get_token)):
    try:
        auth = await discord.isAuthenticated(token)
        return auth
    except Unauthorized:
        return False

@app.exception_handler(Unauthorized)
async def unauthorized_error_handler(_, __):
    return JSONResponse({"error": "Unauthorized"}, status_code=401)

@app.exception_handler(RateLimited)
async def rate_limit_error_handler(_, e: RateLimited):
    return JSONResponse(
        {"error": "RateLimited", "retry": e.retry_after, "message": e.message},
        status_code=429,
    )

@app.get("/user", dependencies=[Depends(discord.requires_authorization)], response_model=User)
async def get_user(user: User = Depends(discord.user)):
    return user

@app.get(
    "/guilds",
    dependencies=[Depends(discord.requires_authorization)],
    response_model=List[GuildPreview],
)
async def get_guilds(guilds: List = Depends(discord.guilds)):
    return guilds
`
javierohern commented 1 year ago

I realized i left out @app.on_event("startup") async def on_startup(): await discord.init() But after adding this I get AttributeError: 'DiscordOAuthClient' object has no attribute 'init'

"EDIT" I got the init problem to go away by installing dependencies from source. With that the @app.on_event("startup") async def on_startup(): await discord.init()

The previous issue is now gone. But after logging in, I get the discord authorization page, and on login my url is appended with ?{code} but I'm still not receiving an access token?

Whole snippet of code ->

`@app.on_event("startup")
async def on_startup():
    await discord.init()

@app.get("/login")
async def login():
    return {"url": discord.oauth_login_url}

@app.get("/callback")
async def callback(code: str):
    token, refresh_token = await discord.get_access_token(code)
    return {"access_token": token, "refresh_token": refresh_token}

@app.get(
    "/authenticated",
    dependencies=[Depends(discord.requires_authorization)],
    response_model=bool,
)
async def isAuthenticated(token: str = Depends(discord.get_token)):
    try:
        auth = await discord.isAuthenticated(token)
        return auth
    except Unauthorized:
        return False

@app.exception_handler(Unauthorized)
async def unauthorized_error_handler(_, __):
    return JSONResponse({"error": "Unauthorized"}, status_code=401)

@app.exception_handler(RateLimited)
async def rate_limit_error_handler(_, e: RateLimited):
    return JSONResponse(
        {"error": "RateLimited", "retry": e.retry_after, "message": e.message},
        status_code=429,
    )

@app.get(
    "/user", dependencies=[Depends(discord.requires_authorization)], response_model=User
)
async def get_user(user: User = Depends(discord.user)):
    return user

@app.get(
    "/guilds",
    dependencies=[Depends(discord.requires_authorization)],
    response_model=List[GuildPreview],
)
async def get_guilds(guilds: List = Depends(discord.guilds)):
    return guilds
`
bigwhitetuna commented 1 year ago

@javierohern I was having a similar issue:

It's coming from this piece:

@app.get("/callback")
async def callback(code: str):
    token, refresh_token = await discord.get_access_token(code)
    return {"access_token": token, "refresh_token": refresh_token}

And more specifically, token, refresh_token = await discord.get_access_token(code).

It appears that there is something going wrong where the discord.get_access_token(code) is not returning a value, which is leaving "token" as None.

If you look at Discord's OAuth2 documentation, you can see how to call for an access token yourself.

Here is what I ended up doing to get around the packaged DiscordOAuthClient.get_access_token returning None:

### DISCORD AUTH ###
dotenv.load_dotenv()
### Identify env variables
DISCORD_CLIENT = os.getenv('DISCORD_CLIENT')
DISCORD_CLIENT_SECRET = os.getenv('DISCORD_CLIENT_SECRET')
DISCORD_REDIRECT_URI = os.getenv('DISCORD_REDIRECT_URI')
DISCORD_TOKEN_URL = "https://discord.com/api/v10/oauth2/token"

discord = DiscordOAuthClient(
    DISCORD_CLIENT, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI, ("identify", "guilds")
)

@app.get("/auth/callback")
async def callback(code: str, request: Request):
    import httpx
    async def exchange_code(code: str):
        payload = {
            "client_id": DISCORD_CLIENT,
            "client_secret": DISCORD_CLIENT_SECRET,
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": DISCORD_REDIRECT_URI,
            "scope": "identify guilds"
        }
        headers: dict = {"Content-Type": "application/x-www-form-urlencoded"}

        async with httpx.AsyncClient() as client:
            resp = await client.post(DISCORD_TOKEN_URL, data=payload, headers=headers)

        return resp.json()

    tokenData = await exchange_code(code)
    logging.info(f"Token data: {tokenData}")

This takes the code that returns with the auth, then uses it to request, and receive, a token. Note that tokenData is still in JSON format, you'll need to parse it as needed.

tokenData looks like this:

{
'token_type': 'Bearer', 
'access_token': stringfortoken', 
'expires_in': 604800, 
'refresh_token': 'd3mFnpt8IIjCmsPiaLO32qFJcdG1Y7', 
'scope': 'identify guilds'
}
Tert0 commented 1 year ago

I could not reproduce this issue with neither v0.2.4 nor v0.2.5.

It would be helpful if you could try to reproduce the following steps:

  1. Clone Repository and Checkout the tag v0.2.5.
    git clone https://github.com/Tert0/fastapi-discord.git
    cd fastapi-discord
    git checkout v0.2.5
  2. Create a Discord Application and add http://127.0.0.1:8000/callback as a redirect URL.
  3. Insert your Client ID, Client Secret into examples/basic.py and use http://127.0.0.1:8000/callback as the redirect URL.
  4. Install the library and uvicorn
    pip3 install fastapi-discord==0.2.5 uvicorn
  5. Run the API Server with
    uvicorn examples.basic
  6. Get the OAuth URL with
    curl http://localhost:8000/login

    Or open http://localhost:8000/login in a browser. Then open the generated URL in a browser and click on authorize.

The browsers should display the access and refresh token in JSON.

{"access_token":"redacted","refresh_token":"redacted"}

I hope it works for you, at least it works for me.

(I have tested it with a docker container python (Debian-based))