79743387

Date: 2025-08-22 12:17:20
Score: 0.5
Natty:
Report link

As it turns out, the problem was not NextCloud. Using this tutorial I implemented a working login flow using only the `requests` package. The code for that is below. It is not yet handling performing any kind of API request using the obtained access token beyond the initial authentication, nor is it handling using the refresh token to get a new access token when the old one expired. That is functionality an oauth library is usually handling and this manual implementation is not doing that for now. However it proves the problem isn't with NextCloud.

I stepped through both the initial authlib implementation and the new with a debugger and the request sent to the NextCloud API for getting the access token looks the same in both cases at first glance. There must be something subtly wrong about the request in the authlib case that causes the API to run into an error. I will investigate this further and take this bug up with authlib. This question here is answered and if there is a bug fix in authlib I will edit the answer to mention which version fixes it.


from __future__ import annotations

from pathlib import Path
import io
import uuid
from urllib.parse import urlencode
import requests
from flask import Flask, render_template, jsonify, request, session, url_for, redirect
from flask_session import Session

app = Flask("webapp")

# app.config is set here, specifically settings:
# NEXTCLOUD_CLIENT_ID
# NEXTCLOUD_SECRET
# NEXTCLOUD_API_BASE_URL
# NEXTCLOUD_AUTHORIZE_URL
# NEXTCLOUD_ACCESS_TOKEN_URL

# set session to be managed server-side
Session(app)


@app.route("/", methods=["GET"])
def index():
    if "user_id" not in session:
        session["user_id"] = "__anonymous__"
        session["nextcloud_authorized"] = False
    return render_template("index.html", session=session), 200

@app.route("/nextcloud_login", methods=["GET"])
def nextcloud_login():
    if "nextcloud_authorized" in session and session["nextcloud_authorized"]:
        redirect(url_for("index"))

    session['nextcloud_login_state'] = str(uuid.uuid4())

    qs = urlencode({
        'client_id': app.config['NEXTCLOUD_CLIENT_ID'],
        'redirect_uri': url_for('callback_nextcloud', _external=True),
        'response_type': 'code',
        'scope': "",
        'state': session['nextcloud_login_state'],
    })

    return redirect(app.config['NEXTCLOUD_AUTHORIZE_URL'] + '?' + qs)

@app.route('/callback/nextcloud', methods=["GET"])
def callback_nextcloud():
    if "nextcloud_authorized" in session and session["nextcloud_authorized"]:
        redirect(url_for("index"))

    # if the callback request from NextCloud has an error, we might catch this here, however
    # it is not clear how errors are presented in the request for the callback
    # if "error" in request.args:
    #     return jsonify({"error": "NextCloud callback has errors"}), 400

    if request.args["state"] != session["nextcloud_login_state"]:
        return jsonify({"error": "CSRF warning! Request states do not match."}), 403

    if "code" not in request.args or request.args["code"] == "":
        return jsonify({"error": "Did not receive valid code in NextCloud callback"}), 400

    response = requests.post(
        app.config['NEXTCLOUD_ACCESS_TOKEN_URL'],
        data={
            'client_id': app.config['NEXTCLOUD_CLIENT_ID'],
            'client_secret': app.config['NEXTCLOUD_SECRET'],
            'code': request.args['code'],
            'grant_type': 'authorization_code',
            'redirect_uri': url_for('callback_nextcloud', _external=True),
        },
        headers={'Accept': 'application/json'},
        timeout=10
    )

    if response.status_code != 200:
        return jsonify({"error": "Invalid response while fetching access token"}), 400

    response_data = response.json()
    access_token = response_data.get('access_token')
    if not access_token:
        return jsonify({"error": "Could not find access token in response"}), 400

    refresh_token = response_data.get('refresh_token')
    if not refresh_token:
        return jsonify({"error": "Could not find refresh token in response"}), 400

    session["nextcloud_access_token"] = access_token
    session["nextcloud_refresh_token"] = refresh_token
    session["nextcloud_authorized"] = True
    session["user_id"] = response_data.get("user_id")

    return redirect(url_for("index"))
Reasons:
  • Blacklisted phrase (1): this tutorial
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Low reputation (0.5):
Posted by: Etienne Ott