Oauthlib: La aplicación web del cliente ya no envía client_id

Creado en 8 sept. 2018  ·  26Comentarios  ·  Fuente: oauthlib/oauthlib

Encontré una regresión en el maestro cuando se usa con solicitudes / solicitudes-oauthlib desde que se fusionó https://github.com/oauthlib/oauthlib/issues/495 . Está relacionado únicamente con la concesión de autorización / aplicación web.

El uso básico de request-oauthlib es:

sess = OAuth2Session(client_id)
token = sess.fetch_token(token_url, client_secret=client_secret, authorization_response=request.url)

Sin embargo, desde los cambios, se ignora client_id de la sesión. Creo que https://github.com/oauthlib/oauthlib/pull/505 solucionó un caso de uso pero rompió otro. Deberíamos encontrar una solución en la que todos ganen.

request-oauthlib code call en https://github.com/requests/requests-oauthlib/blob/master/requests_oauthlib/oauth2_session.py#L196 -L198 y problema de oauthlib aquí
https://github.com/oauthlib/oauthlib/blame/master/oauthlib/oauth2/rfc6749/clients/web_application.py#L128.

Bug Discussion OAuth2-Client

Comentario más útil

No vería cómo querría anular client_id con un valor diferente, por lo que votaría a favor de generar una excepción si difieren.

¿Deberíamos, además, registrar un DeprecationWarning si client_id se proporcionó como un kwarg?

Todos 26 comentarios

Mi primer pensamiento es revertir los cambios en prepare_request_body para, de forma predeterminada, usar el conjunto self.client_id en el constructor WebApplicationClient .
Luego, los documentos en línea deben cambiarse para agregar &client_id=xx a la salida prepare_request_body .

Finalmente, para reemplazar la solución original, sugeriré eliminar client_id de los argumentos y agregar un nuevo argumento a prepare_request_body como include_client=True/False para agregar client_id y client_secret en el cuerpo, o no incluir ambos.

¿Pensamientos?

empujar @Diaoul @skion @thedrow

Qué pasa:

Sugeriré eliminar client_id de los argumentos y agregar un nuevo argumento a prepare_request_body como include_client = True / False para agregar client_id y client_secret en el cuerpo, o no incluir ambos.

Gracias

De hecho, activé este mismo problema en una de mis pruebas, pero lo presenté contra las solicitudes / oauthlib aquí: https://github.com/requests/requests-oauthlib/issues/330

Creo que el problema es culpa de request_oauthlib. Sus documentos, en realidad el primer ejemplo en todos sus documentos cuando carga la página, admiten la especificación de client_id en el constructor OAuth2Session . La lógica en la línea 200 extrae el client_id de los kwargs, pero no tiene una alternativa para extraerlo de la instancia WebApplicationClient ya construida.

@jvanasco , el problema actual # 585 se puede solucionar en las solicitudes-oauthlib solo, o revirtiendo el PR # 505 de oauthlib. Sin embargo, ninguna de las soluciones solucionará el comportamiento mencionado por @skion en su comentario en https://github.com/oauthlib/oauthlib/pull/505#issuecomment -351221107

De acuerdo con la especificación, el parámetro client_id debe enviarse para clientes no autenticados, pero preferiblemente NO se envía en el cuerpo de solicitud de token para clientes confidenciales, ya que en ese caso el mecanismo preferido para autenticar al cliente es a través de la autenticación básica HTTP. Sin embargo, la clase WebApplication siempre lo incluye (lo que rompe algunos servidores) y no ofrece ningún mecanismo para eliminarlo.

Oauthlib debe proporcionar una forma elegante y sencilla para que las solicitudes-oauthlib resuelvan el problema. Si podemos encontrar una solución en esta discusión, será genial; porque eso es un gran bloqueador.

¿Permitir client_id=False en prepare_request_body() help, indicar que no se enviará un client_id? Aunque incluso si es así, eso conduciría a esta fealdad cerca de la línea 126 :

client_id = None if client_id=False else self.client_id

Ah, ya veo.

¿Existe una prueba de unidad existente para cuando se debe enviar client_id o no? Si no es así, ¿alguien tiene una lista que pueda usarse para generar una? Estaría feliz de intentar arreglar esto y las solicitudes-oauthlib, porque está bloqueando parte de mi trabajo en este momento.

@JonathanHuot

Sugeriré eliminar client_id de los argumentos y agregar un nuevo argumento a prepare_request_body como include_client = True / False para agregar client_id y client_secret en el cuerpo, o no incluir ambos.

Leyendo esto, en realidad me gusta bastante tu sugerencia. Probablemente podamos hacerlo sin interrupciones, ya que la función ya toma **kwargs .

Una nota:

para agregar client_id y client_secret en el cuerpo

Dado que se trata de clientes públicos IIUC, no creo que el client_secret esté involucrado; ¿Es solo el client_id se agrega al cuerpo? En ese caso, también consideraría cambiar el nombre del nuevo parámetro a include_client_id=True/False .

En ese caso, también consideraría cambiar el nombre del nuevo parámetro a include_client_id = True / False.

En efecto ! client_secret no está involucrado ya que no está presente en WebApplicationClient .

@jvanasco , si quieres hacer un PR, creo que los cambios deberían ser:
1) Revertir https://github.com/oauthlib/oauthlib/pull/505
2) Cambie la firma de prepare_request_body() para eliminar client_id y agregar include_client_id=True/False ( True (predeterminado): agrega self.client_id )

Entonces, request-oauthlib tendrá la opción en https://github.com/requests/requests-oauthlib/blob/master/requests_oauthlib/oauth2_session.py#L196-L211 para:
A) Incluya client_id solo en el cuerpo

self._client.prepare_request_body(..)

B) Incluya client_id y client_secret en auth y no los incluya en el cuerpo (solución preferida de RFC)

self._client.prepare_request_body(include_client_id=False, ..)
auth = requests.auth.HTTPBasicAuth(client_id, client_secret)

C) Incluya client_id y client_secret en el cuerpo (solución alternativa de RFC)

self._client.prepare_request_body(client_secret=client_secret, ..)

Generaré relaciones públicas para ambos proyectos hoy.

Prácticamente tengo relaciones públicas y pruebas realizadas para OAuthlib. Aunque tengo una pregunta ...

¿Debería permitirse client_id como kwarg? Esto es en parte por compatibilidad con versiones anteriores, pero también para casos extremos. Debido a que este método estaba algo roto, creo que puede valer la pena hacerlo funcionar según lo previsto (como permitir que en prepare_request_body anule el self.client_id) o generar una excepción en el uso incorrecto (como en el aumento de un error si se proporciona client_id pero no coincide con self.client_id ).

No vería cómo querría anular client_id con un valor diferente, por lo que votaría a favor de generar una excepción si difieren.

¿Deberíamos, además, registrar un DeprecationWarning si client_id se proporcionó como un kwarg?

PR # 593 presentado. Genera una DeprecationWarning cuando se envía client_id , y un ValueError si difiere de self.client_id . También hay una nueva prueba que asegura el cumplimiento de los tres escenarios detallados por @JonathanHuot .

encontré mi primer problema con las solicitudes de candidato de relaciones públicas de oauthlib mientras escribía algunas pruebas

SIEMPRE invoca prepare_request_body con username=username, password=password . Esto parece incorrecto. Espero que alguien aquí esté más familiarizado con el RFC y sepa las respuestas a lo siguiente:

  1. ¿Debería un username + password incluso ser un parámetro en el request.body?
  2. Si username + password aparecen en el encabezado HTTP Basic Auth, ¿deberían estar duplicados en el cuerpo de la solicitud?
  3. ¿Es posible tener parámetros username + password body y un encabezado HTTPBasicAuth con los detalles del cliente?

Gracias por encontrarte con esto.

  1. username y password siempre deben estar presentes en el cuerpo de la solicitud. Deben usarse solo para la concesión de contraseñas, también conocido como legado. Esos no deben usarse para otras concesiones (implícitas, código, credenciales de cliente).
  2. La autenticación básica HTTP es opcional para las credenciales de cliente (recomendado para clientes confidenciales, es decir, con client_secret ). Las credenciales de usuario nunca deben estar en HTTP Basic Auth.

Gracias. Solo para aclarar dos cosas, y siéntase libre de hablarme como si tuviera cinco años. Quiero asegurarme de obtener esto y las pruebas correctas:

  1. El nombre de usuario y la contraseña solo se utilizan en un cierto tipo de concesión que los requiera. Si se utilizan, solo pueden estar presentes en el cuerpo de la solicitud.

A menos que se especifiquen explícitamente, no deberían estar presentes, ¿correcto?

  1. Si la autenticación básica de Http es solo para las credenciales del cliente, entonces las solicitudes-oauthlib existentes no deberían tener el bloque que genera una autenticación básica a partir del combo de nombre de usuario y contraseña.

Por favor, perdóname por ser prolijo y obsesionado con estos pequeños detalles. Solo quiero asegurarme de que obtengo el comportamiento correcto y puedo escribir pruebas que aseguren que no obtengamos otra regresión.

@jvanasco , puedo hablar sobre OAuth2 RFC, sin embargo, no estoy seguro de cómo encajan requests-oauthlib y flask-oauthlib .

Correcto.

  1. Sí, AFAIU no debería tener este bloque https://github.com/requests/requests-oauthlib/blob/master/requests_oauthlib/oauth2_session.py#L207 -L211.

Tengo entendido, sin embargo, será bueno cotejar con la realidad del campo; es decir, solicitudes-oauthlib y experiencias de diferentes proveedores públicos. Discusiones sobre solicitudes múltiples oauthlib https://github.com/requests/requests-oauthlib/issues/218 , https://github.com/requests/requests-oauthlib/issues/211 , https://github.com/requests / request-oauthlib / issues / 264 , ya sucedió.

Creo que tenían una confusión entre client password y client secret que en realidad son dos palabras para exactamente lo mismo.
Si seguimos la lógica detrás de https://github.com/requests/requests-oauthlib/pull/206 , el contenido del PR nunca debería haber sido como agregar HTTPAuth(username, password) pero debería haber sido HTTPAuth(client_id, client_secret (la contraseña del cliente).

Poke @Lukasa , @chaosct , @ibuchanan que participó en las discusiones de las solicitudes-oauthlib.

¡Excelente! muchas gracias. Pensé que eso era lo que estaba pasando, pero quería confirmarlo.

Creo que sé cómo quiero estructurar las solicitudes ahora. Tengo un puñado de confirmaciones en el proyecto de solicitudes principales, por lo que sé lo que a los mantenedores les gusta ver en las relaciones públicas y la funcionalidad.

  1. El nombre de usuario y la contraseña solo se utilizan en un cierto tipo de concesión que los requiera. Si se utilizan, solo pueden estar presentes en el cuerpo de la solicitud.

Sí, a la primera parte: solo un cierto tipo de subvención las requiere. Pero en la segunda parte sobre cómo enviarlos en el cuerpo de la solicitud, la especificación dice:

Incluir las credenciales del cliente en el cuerpo de la solicitud
NO SE RECOMIENDA usar los dos parámetros
y DEBE limitarse a los clientes que no puedan utilizar directamente
el esquema de autenticación HTTP Basic ...

Pero para los servidores, dice:

El servidor de autorización PUEDE admitir
las credenciales del cliente en el cuerpo de la solicitud ...

Un cliente compatible, no enviaría las credenciales en el cuerpo de la solicitud.
Pero, para algunos servidores implementados parcialmente, solo los aceptarán en el cuerpo de la solicitud.
Si recuerdo correctamente, mi RP resolvió esta confusión agregando el encabezado de autenticación,
por lo que el cliente envía ambos.

Creo que @JonathanHuot tiene razón sobre el segundo punto.

Hola @ibuchanan , las citas a las que te refieres usan el término client credentials . Debemos tener mucho cuidado de no mezclar al cliente con el propietario del recurso (el usuario real).

Los roles se explican claramente aquí rfc6749 # 1.1 .
Este client credentials refiere a client_id y client_secret y el propietario del recurso se refiere a username y password . Esos no son intercambiables.

Entonces, al leer el RFC con esos roles significa que un cliente compatible DEBE enviar las credenciales del cliente ( client_id , client_secret ) en HTTP Basic y DEBE enviar las credenciales del usuario ( username , password ) en el cuerpo de la solicitud (nunca lea una alternativa aquí); ver rfc6749 # 4.3.2 .

Algún servidor rechaza la solicitud (400) si el client_id está en la solicitud
cuerpo. Creo que el valor predeterminado debería ser el recomendado por la especificación.

Está bien. Creo que el PR actual para oauthlib satisface las preocupaciones anteriores: la bandera include_client_id permite explícitamente que se envíe o no client_id .

en términos de requests_oauthlib , esto es lo que estoy pensando:

  1. username y password solo aparecerán en el cuerpo (no como HTTP Basic). Si ese comportamiento es necesario para una integración de servidor no compatible, el implementador puede enviar un argumento auth o headers a fetch_token() .

  2. suministrar el client_id en el lugar correcto es un poco molesto, pero creo que tengo la lógica y los casos de uso abajo. esto definitivamente necesitará alguna revisión.

Una pregunta que tengo para @JonathanHuot y @ibuchanan :

oauthlib 's OAuth2 Client y requests_oauthlib OAuth2Session no conservan el client_secret y deben invocarlo repetidamente. Este no fue el caso en OAuth1, y creo que este fue el problema real detrás del # 370. El RFC no mencionó la necesidad de esto y no pude encontrar ningún historial.

Para mí, tiene sentido extender el Cliente con el almacenamiento de client_secret , y comenzar a desaprobar la dependencia de OAuth2Session de pasar el client_secret en fetch_token y request a favor de usar el que está ahora en el propio Cliente. (esto también abordaría algunas otras inconsistencias informadas en https://github.com/requests/requests-oauthlib/issues/264)

Cambié ligeramente el PR para oauthlib (# 593) para estandarizar las variables de prueba para nombre de usuario / contraseña y client_id / client_secret. Creo que debería evitar errores de confusión entre los dos en el futuro.

El cambio propuesto para las solicitudes-oauthlib: https://github.com/requests/requests-oauthlib/compare/master...jvanasco : fix-oauthlib_client_id

Este es un poco más drástico, porque mirando el código y las pruebas, parece que la biblioteca solo estaba tratando de hacer que todo tipo de cosas funcionaran que no deberían.

La solución hace algunas cosas:

  1. La verificación de username y password ocurre solo para LegacyApplicationClient instancias, ya que ese debería ser el único tipo necesario (¿correcto?). Hay una sección debajo del cheque que fusiona el nombre de usuario / contraseña en el dict kwargs. Actualmente está desactualizado, porque las pruebas sugieren que otros clientes podrían querer pasar esta información.

  2. La lógica para manejar los encabezados client_id / auth se reescribió para garantizar que los tipos correctos de autenticación ocurran en el lugar correcto de forma predeterminada. Si un usuario desea forzar las credenciales de otra manera, aún es posible.

  3. Pregunta: ¿Existe alguna situación en la que client_secret no se pasen? No puedo pensar en ninguno, pero hay muchos flujos de oAuth.

Entonces, en la versión de solicitudes-oauthlib:

| include_client_id | auth | comportamiento |
| ------------------- | --------------- | -------- |
| Ninguno (predeterminado) | Ninguno (predeterminado) | cree un objeto Auth con client_id . No envíe el client_id en el cuerpo. Este es el comportamiento predeterminado, porque la RFC lo recomienda. |
| Ninguno (predeterminado) | presente | utilice el objeto Auth. invocar prepare_request_body oauthlib con include_client_id=False |
| Falso presente | utilice el objeto Auth. invocar prepare_request_body oauthlib con include_client_id=False |
| Verdadero | presente | utilice el objeto Auth. invocar prepare_request_body oauthlib con include_client_id=True |
| Verdadero | Ninguno (predeterminado) | cree un objeto Auth con client_id . invocar prepare_request_body oauthlib con include_client_id=True |
| Falso Ninguno (predeterminado) | cree un objeto Auth con client_id . invocar prepare_request_body oauthlib con include_client_id=False |

o dicho de otra manera:

  • crear un objeto de autenticación

    • no envíe la identificación del cliente en el cuerpo:

    • (include_client_id = Ninguno, auth = Ninguno)

    • (include_client_id = False, auth = None)

    • envíe la identificación del cliente en el cuerpo:

    • (include_client_id = True, auth = None)

  • usar un objeto de autenticación explícito

    • no envíe el client_id en el cuerpo:

    • (include_client_id = Ninguno, auth = authObject)

    • (include_client_id = False, auth = authObject)

    • envíe el client_id en el cuerpo:

    • (include_client_id = True, auth = authObject)

@jvanasco : se ve muy bien en el lado oauthlib y las solicitudes-oauthlib.

Acerca de su 3. pregunta:

Puede tener clientes públicos sin client_secret (o vacío, eso es cercano desde el punto de vista del RFC); por lo que la API de Python debe admitir "ningún secreto".

El caso de uso del mundo real suele ser para aplicaciones nativas en las que prefiere usar un código de autorización, pero no puede garantizar la seguridad en torno a mantener el secreto a salvo, por lo tanto, acepta un client_id sin client_secret (o client_secret vacíos que son idénticos para el RFC); o también tiene otro PKCE RFC disponible (consulte https://oauth.net/2/pkce/). Pero este último aún no está implementado en el lado de oauthlib .

Gracias. Agregaré algunos casos de prueba para asegurar que podamos enviar client_id sin client_secret . Perdón por hacer tantas preguntas, hay tantas implementaciones correctas posibles de esta especificación (e incluso más implementaciones rotas que necesitan funcionar).

@JonathanHuot la implementación existente no admite el envío de una cadena vacía por client_secret . Se elimina en esta lógica https://github.com/oauthlib/oauthlib/blob/master/oauthlib/oauth2/rfc6749/parameters.py#L90 -L125 - específicamente línea 122

Para respaldarlo, puede agregar algo como esto justo después de esa rutina:

if ('client_secret' in kwargs) and ('client_secret' not in params):
    if kwargs['client_secret'] == '':
        params.append((unicode_type('client_secret'), kwargs['client_secret']))

Eso enviaría una cadena vacía por client_secret cuando el secreto es una cadena vacía, pero no enviaría client_secret si el valor es None .

Creo que vale la pena respaldar esto, porque si el RFC admite cualquiera de las dos variantes ... es probable que haya muchas implementaciones rotas que solo admitan una variante.

Este problema original está solucionado en oauthlib. Sin embargo, el comportamiento sigue ahí hasta que se corrija https://github.com/requests/requests-oauthlib/pull/331 .

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

JonathanHuot picture JonathanHuot  ·  33Comentarios

prudnikov picture prudnikov  ·  11Comentarios

polamayster picture polamayster  ·  19Comentarios

ryarnyah picture ryarnyah  ·  3Comentarios

thedrow picture thedrow  ·  31Comentarios