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.
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
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
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.
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
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.