paint-brush
Cree su propio servidor OAuth2 en Gopor@cyantarek
45,888 lecturas
45,888 lecturas

Cree su propio servidor OAuth2 en Go

por Cyan Tarek9m2019/02/06
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

OAuth2 es un tipo de protocolo o marco para proteger los servicios web RESTful. La mayoría de las API REST están protegidas con OAuth 2 debido a su sólida seguridad. Construir un servidor OAuth no es fácil pero sí jugoso. Construir su propio servidor OAuth no es fácil ni difícil. Construiremos el nuestro. Será muy útil si desea crear una API pública o privada lista para producción. Crear un servidor de recursos REST es una buena manera de proteger sus recursos del acceso no autorizado.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - Cree su propio servidor OAuth2 en Go
Cyan Tarek HackerNoon profile picture

Hola, en el artículo de hoy, te mostraré cómo puedes construir tu propio servidor OAuth2 como Google, Facebook, Github, etc.

Esto será muy útil si desea crear una API pública o privada lista para producción. Entonces empecemos.

¿Qué es OAuth2?

La versión 2.0 de Open Authorization se conoce como OAuth2. Es un tipo de protocolo o marco para proteger los servicios web RESTful. OAuth2 es muy poderoso. Hoy en día, la mayoría de las API REST están protegidas con OAuth2 debido a su sólida seguridad.

OAuth2 tiene dos partes

01. Cliente

02. Servidor

Cliente OAuth2

Si estás familiarizado con esta pantalla, sabes de lo que estoy hablando. De todos modos, déjame explicarte la historia detrás de la imagen:

Está creando una aplicación orientada al usuario que funciona con los repositorios de github del usuario. Por ejemplo: herramientas de CI como TravisCI, CircleCI, Drone, etc.

Pero la cuenta de github del usuario está protegida y nadie puede acceder a ella si el propietario no lo desea. Entonces, ¿cómo acceden estas herramientas de CI a la cuenta y los repositorios de github del usuario?

Fácil.

Su aplicación le preguntará al usuario

“Para trabajar con nosotros, debe otorgar acceso de lectura a sus repositorios de github. ¿Estás de acuerdo?"

Entonces el usuario dirá

"Sí. Y haz lo que tengas que hacer”.

Luego, su aplicación se comunicará con la autoridad de github para otorgar acceso a la cuenta de github de ese usuario en particular. Github comprobará si es cierto y le pedirá al usuario que lo autorice. Luego, github emitirá un token efímero para el cliente.

Ahora, cuando su aplicación necesite acceder después de la autenticación y autorización, debe enviar el token de acceso con la solicitud para que github piense:

“Oh, el token de acceso parece familiar, puede ser que te lo hayamos dado. Ok, puedes acceder”

Esa es la larga historia. Los días han cambiado, ahora no necesitas ir físicamente a la autoridad de github cada vez (nunca tuvimos que hacer eso). Todo se puede hacer automáticamente.

¿Pero cómo?

Este es un diagrama de secuencia UML de lo que he hablado hace un par de minutos. Solo representación gráfica.

De la imagen de arriba, encontramos algunas cosas importantes.

OAuth2 tiene 4 funciones:

01. Usuario: el usuario final que usará su aplicación

02. Cliente: la aplicación que está creando que usará la cuenta de github y el usuario usará

03. Servidor de autenticación: el servidor que se ocupa de las cosas principales de OAuth

04. Servidor de recursos: el servidor que tiene los recursos protegidos. Por ejemplo github

El cliente envía una solicitud OAuth2 al servidor de autenticación en nombre del usuario.

Crear un cliente OAuth2 no es fácil ni difícil. Suena divertido, ¿verdad? Lo haremos en la siguiente parte.

Pero en esta parte, nos iremos al otro lado del mundo. Construiremos nuestro propio servidor OAuth2. Que no es fácil pero sí jugoso.

¿Listo? Vamos

Servidor OAuth2

puedes preguntarme

"Espera un minuto, Cyan, ¿por qué construir un servidor OAuth2?"

¿Te olvidaste hombre? He dicho esto antes. Bien, déjame decirte de nuevo.

Imagínese, está creando una aplicación muy útil que brinda información meteorológica precisa (hay muchas API de este tipo). Ahora desea abrirlo para que el público pueda usarlo o quiere ganar dinero con él.

Sea cual sea el caso, debe proteger sus recursos de accesos no autorizados o ataques maliciosos. Para hacer eso, necesita asegurar sus recursos de API. Aquí viene la cosita de OAuth2. ¡Bingo!

En la imagen de arriba, podemos ver que necesitamos colocar un servidor de autenticación frente a nuestro servidor de recursos API REST. De eso estamos hablando. El servidor de autenticación se creará utilizando la especificación OAuth2. Entonces nos convertiremos en el github de la primera imagen, jajajaja es broma.

El objetivo principal del servidor OAuth2 es proporcionar un token de acceso al cliente. Es por eso que el servidor OAuth2 también se conoce como proveedor OAuth2, porque proporciona token.

Basta de hablar.

Hay cuatro tipos de servidores OAuth2 basados en el tipo Grant Flow:

01. Concesión de código de autorización

02. Concesión implícita

03. Otorgamiento de Credenciales de Cliente

04. Concesión de contraseña

Si desea obtener más información sobre OAuth2, consulte este increíble artículo.

Para este artículo, usaremos el tipo de concesión de credenciales de cliente . Así que profundicemos

Servidor basado en flujo de concesión de credenciales de cliente

Al implementar el servidor OAuth2 basado en flujo de otorgamiento de credenciales de cliente, necesitamos saber un par de cosas.

En este tipo de concesión, no hay interacción del usuario (es decir, registro, inicio de sesión). Se necesitan dos cosas, y son client_id y client_secret . Con estas dos cosas, podemos obtener access_token . El cliente es la aplicación de terceros. Cuando necesita acceder al servidor de recursos sin el usuario o solo mediante la aplicación cliente, este tipo de concesión es simple y el más adecuado.

Aquí hay un diagrama de secuencia UML de la misma.

Codificación

Para construir esto, necesitamos confiar en un increíble paquete Go

En primer lugar, construyamos un servidor API simple como servidor de recursos

 package main import ( "log" "net/http" ) func main() { http.HandleFunc( "/protected" , func(w http.ResponseWriter, r *http.Request) { w.Write([]byte( "Hello, I'm protected" )) }) log.Fatal(http.ListenAndServe( ":9096" , nil))

Ejecute el servidor y envíe una solicitud de obtención a http://localhost:9096/protected

Obtendrá respuesta.

¿Qué tipo de servidor protegido es?

Aunque el nombre del punto final está protegido, cualquiera puede acceder a él. Entonces necesitamos protegerlo con OAuth2.

Ahora escribiremos nuestro servidor de autorizaciones

Rutas

01. /credentials para emitir credenciales de cliente (client_id y client_secret)

03. /token para emitir token con credenciales de cliente

Necesitamos implementar estas dos rutas.

Aquí está la configuración preliminar

 package main import ( "encoding/json" "fmt" "github.com/google/uuid" "gopkg.in/oauth2.v3/models" "log" "net/http" "time" "gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/store" ) func main() { manager := manage.NewDefaultManager() manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) manager.MustTokenStorage(store.NewMemoryTokenStore()) clientStore := store.NewClientStore() manager.MapClientStorage(clientStore) srv := server.NewDefaultServer(manager) srv.SetAllowGetAccessRequest( true ) srv.SetClientInfoHandler(server.ClientFormHandler) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { log.Println( "Internal Error:" , err.Error()) return }) srv.SetResponseErrorHandler(func(re *errors.Response) { log.Println( "Response Error:" , re.Error.Error()) }) http.HandleFunc( "/protected" , func(w http.ResponseWriter, r *http.Request) { w.Write([]byte( "Hello, I'm protected" )) }) log.Fatal(http.ListenAndServe( ":9096" , nil)) }

Aquí creamos un administrador, una tienda de clientes y el propio servidor de autenticación.

Aquí está la ruta /credenciales

 http.HandleFunc( "/credentials" , func(w http.ResponseWriter, r *http.Request) { clientId := uuid.New().String()[: 8 ] clientSecret := uuid.New().String()[: 8 ] err := clientStore.Set(clientId, &models.Client{ ID : clientId, Secret : clientSecret, Domain : "http://localhost:9094" , }) if err != nil { fmt.Println(err.Error()) } w.Header().Set( "Content-Type" , "application/json" ) json.NewEncoder(w).Encode(map[string]string{ "CLIENT_ID" : clientId, "CLIENT_SECRET" : clientSecret}) })

Crea dos cadenas aleatorias, una para client_id y otra para client_secret. Luego los guarda en la tienda del cliente. Y devolverlos como respuesta. Eso es todo. Usamos en el almacén de memoria, pero podemos almacenarlos en redis, mongodb, postgres, etc.

Aquí está la ruta /token :

 http.HandleFunc( "/token" , func(w http.ResponseWriter, r *http.Request) { srv.HandleTokenRequest(w, r) })

Es muy simple. Pasa la solicitud y la respuesta al controlador apropiado para que el servidor pueda decodificar todos los datos necesarios de la carga útil de la solicitud.

Así que aquí está nuestro código general:

 package main import ( "encoding/json" "fmt" "github.com/google/uuid" "gopkg.in/oauth2.v3/models" "log" "net/http" "time" "gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/store" ) func main() { manager := manage.NewDefaultManager() manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) manager.MustTokenStorage(store.NewMemoryTokenStore()) clientStore := store.NewClientStore() manager.MapClientStorage(clientStore) srv := server.NewDefaultServer(manager) srv.SetAllowGetAccessRequest( true ) srv.SetClientInfoHandler(server.ClientFormHandler) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { log.Println( "Internal Error:" , err.Error()) return }) srv.SetResponseErrorHandler(func(re *errors.Response) { log.Println( "Response Error:" , re.Error.Error()) }) http.HandleFunc( "/token" , func(w http.ResponseWriter, r *http.Request) { srv.HandleTokenRequest(w, r) }) http.HandleFunc( "/credentials" , func(w http.ResponseWriter, r *http.Request) { clientId := uuid.New().String()[: 8 ] clientSecret := uuid.New().String()[: 8 ] err := clientStore.Set(clientId, &models.Client{ ID : clientId, Secret : clientSecret, Domain : "http://localhost:9094" , }) if err != nil { fmt.Println(err.Error()) } w.Header().Set( "Content-Type" , "application/json" ) json.NewEncoder(w).Encode(map[string]string{ "CLIENT_ID" : clientId, "CLIENT_SECRET" : clientSecret}) }) http.HandleFunc( "/protected" , func(w http.ResponseWriter, r *http.Request) { w.Write([]byte( "Hello, I'm protected" )) }) log.Fatal(http.ListenAndServe( ":9096" , nil)) }

Ejecute el código y vaya a la ruta http://localhost:9096/credentials para registrarse y obtener client_id y client_secret

Ahora vaya a esta URL http://localhost:9096/token?grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&scope=all

Obtendrá el access_token con el tiempo de caducidad y alguna otra información.

Ahora tenemos nuestro access_token. Pero nuestra ruta /protected aún no está protegida. Necesitamos configurar una forma que verifique si existe un token válido con cada solicitud del cliente. En caso afirmativo, le damos acceso al cliente. De otra forma no.

Podemos hacer esto con un middleware.

Escribir middleware en go es muy divertido si sabes lo que estás haciendo. Aquí está el software intermedio:

 func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := srv.ValidationBearerToken(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } f.ServeHTTP(w, r) }) }

Esto verificará si se proporciona un token válido con la solicitud y tomará medidas en función de eso.

Ahora necesitamos colocar este middleware frente a nuestra ruta /protected usando el patrón de adaptador/decorador

 http.HandleFunc( "/protected" , validateToken(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte( "Hello, I'm protected" )) }, srv))

Ahora todo el código se ve así:

 package main import ( "encoding/json" "fmt" "github.com/google/uuid" "gopkg.in/oauth2.v3/models" "log" "net/http" "time" "gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/store" ) func main() { manager := manage.NewDefaultManager() manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) // token memory store manager.MustTokenStorage(store.NewMemoryTokenStore()) // client memory store clientStore := store.NewClientStore() manager.MapClientStorage(clientStore) srv := server.NewDefaultServer(manager) srv.SetAllowGetAccessRequest( true ) srv.SetClientInfoHandler(server.ClientFormHandler) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { log.Println( "Internal Error:" , err.Error()) return }) srv.SetResponseErrorHandler(func(re *errors.Response) { log.Println( "Response Error:" , re.Error.Error()) }) http.HandleFunc( "/token" , func(w http.ResponseWriter, r *http.Request) { srv.HandleTokenRequest(w, r) }) http.HandleFunc( "/credentials" , func(w http.ResponseWriter, r *http.Request) { clientId := uuid.New().String()[: 8 ] clientSecret := uuid.New().String()[: 8 ] err := clientStore.Set(clientId, &models.Client{ ID : clientId, Secret : clientSecret, Domain : "http://localhost:9094" , }) if err != nil { fmt.Println(err.Error()) } w.Header().Set( "Content-Type" , "application/json" ) json.NewEncoder(w).Encode(map[string]string{ "CLIENT_ID" : clientId, "CLIENT_SECRET" : clientSecret}) }) http.HandleFunc( "/protected" , validateToken(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte( "Hello, I'm protected" )) }, srv)) log.Fatal(http.ListenAndServe( ":9096" , nil)) } func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := srv.ValidationBearerToken(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } f.ServeHTTP(w, r) }) }

Ahora ejecute el servidor e intente acceder a /punto final protegido sin access_token como consulta de URL. Luego intente dar access_token incorrecto. De cualquier manera, el servidor de autenticación lo detendrá.

Ahora obtenga las credenciales y access_token nuevamente del servidor y envíe la solicitud al punto final protegido:

http://localhost:9096/test?access_token=SU_TOKEN_ACCESO

¡Bingo! Tendrás acceso a él.

Así que hemos aprendido a configurar nuestro propio servidor OAuth2 usando Go.

En la siguiente parte, construiremos nuestro cliente OAuth2 en Go. Y en la última parte, crearemos el servidor basado en el tipo de concesión de código de autorización con el inicio de sesión y la autorización del usuario.