@@ -1,216 +1,92 @@
package auth
import (
"encoding/json"
"io/ioutil"
"context"
"net/http"
"net/url"
"os"
"regexp"
"time"
"strings"
"github.com/andela/micro-api-gateway/log"
"github.com/labstack/echo"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
const (
redirectStatusCode = 302
keyToken = "oauth2_token"
keyNextPage = "redirect_url"
googleUserInfoURL = "https://www.googleapis.com/plus/v1/people/me?access_token="
)
"google.golang.org/grpc/metadata"
var (
// PathLogin is the path to handle OAuth 2.0 logins.
PathLogin = "/login"
// PathLogout is the path to handle OAuth 2.0 logouts.
PathLogout = "/logout"
// PathCallback is the path to handle callback from OAuth 2.0 backend
// to exchange credentials.
PathCallback = "/auth/google/callback"
pathExchange = "/token"
cookie * http.Cookie
"github.com/andela/micro-api-gateway/pb/authorization"
"github.com/andela/micro-api-gateway/pb/user"
"github.com/labstack/echo"
)
func GoogleAuthFromConfig (keyPath string ) echo.MiddlewareFunc {
// NewOAuth2Provider returns a generic OAuth 2.0 backend endpoint.
func Authorize (fn func (string ) (Claims , error )) echo.MiddlewareFunc {
return func (next echo.HandlerFunc ) echo.HandlerFunc {
return func (c echo.Context ) error {
conf := googleAuthConfig (keyPath )
switch c .Request ().URL .Path {
case PathLogin :
return login (conf , c )
case PathLogout :
return logout (c )
case pathExchange :
return exchange (c )
case PathCallback :
return handleOAuth2Callback (conf , c )
default :
if c .Path () == "/favicon.ico" {
return nil
}
if c .Path () == "/" {
return next (c )
}
}
}
}
func login (f * oauth2.Config , c echo.Context ) error {
to := c .QueryParam (keyNextPage )
return c .Redirect (redirectStatusCode , f .AuthCodeURL (to ))
}
func logout (c echo.Context ) error {
to := c .QueryParam (keyNextPage )
cookie , _ = c .Cookie ("jwt-token" )
cookie = & http.Cookie {
Name : "jwt-token" ,
Value : "" ,
Domain : os .Getenv ("COOKIE_DOMAIN" ),
Path : "/" ,
Expires : time .Now (),
MaxAge : - 1 ,
}
c .SetCookie (cookie )
return c .Redirect (redirectStatusCode , to )
}
func exchange (c echo.Context ) error {
accessToken := c .QueryParam ("google_token" )
response , err := http .Get (googleUserInfoURL + accessToken )
if err != nil {
return c .JSON (http .StatusBadRequest , echo.Map {"error" : err .Error ()})
}
defer response .Body .Close ()
contents , err := ioutil .ReadAll (response .Body )
if c .Path () == "/health" {
return next (c )
}
if c .Request ().Header .Get ("x-forwarded-proto" ) == "http" {
to := "https://" + c .Request ().Host + c .Request ().URL .RequestURI ()
return c .Redirect (redirectStatusCode , to )
}
if err != nil {
return c .JSON (http .StatusBadRequest , echo.Map {"error" : err .Error ()})
}
gUser := GoogleUser {}
err = json .Unmarshal (contents , & gUser )
user := gUser .toUserService (accessToken )
token , err := generateToken (user )
if err != nil {
return c .JSON (http .StatusBadRequest , echo.Map {"error" : err .Error ()})
return handleOtherRouteAccess (fn , next , c )
}
}
return c .JSON (http .StatusOK , echo.Map {"token" : token })
}
func handleOAuth2Callback (f * oauth2.Config , c echo.Context ) error {
next := c .QueryParam ("state" )
code := c .QueryParam ("code" )
t , err := f .Exchange (oauth2 .NoContext , code )
if err != nil {
return redirectWithError ("exchange oauth token failed" , next , c , err )
}
response , err := http .Get (googleUserInfoURL + t .AccessToken )
if err != nil {
return redirectWithError ("fetch user info failed" , next , c , err )
}
defer response .Body .Close ()
contents , err := ioutil .ReadAll (response .Body )
if err != nil {
return redirectWithError ("readAll response Body failed" , next , c , err )
}
gUser := GoogleUser {}
err = json .Unmarshal (contents , & gUser )
user := gUser .toUserService (t .AccessToken )
token , err := generateToken (user )
if err != nil {
return redirectWithError ("failed to create user token" , next , c , err )
}
func handleOtherRouteAccess (fn func (string ) (Claims , error ), next echo.HandlerFunc , c echo.Context ) error {
var claims Claims
if apiToken := c .Request ().Header .Get ("api-token" ); apiToken != "" {
u , err := usersClient .ValidateAPIToken (context .Background (), & user.APIToken {Token : apiToken })
if err != nil {
return respondWithError (401 , "invalid api token" , c )
}
claims = NewClaims (u )
claims .Permissions , _ = getPermission (u .Roles )
} else {
var tokenString string
jwtToken , err := c .Cookie ("jwt-token" )
// Set cookie if andela subdomain. Return token in url if not running on
// andela's subdomain or if mobile app
if match , err := regexp .MatchString (`.*andela\.(com|me)` , next ); err == nil && match {
if err == nil {
cookie = & http.Cookie {
Name : "jwt-token" ,
Value : token ,
Domain : os .Getenv ("COOKIE_DOMAIN" ),
Path : "/" ,
Expires : time .Now ().Add (time .Hour * 72 ),
if err != nil || jwtToken == nil {
if tokenString = getTokenFromRequest (c .Request ()); tokenString == "" {
return respondWithError (401 , "token not present" , c )
}
c .SetCookie (cookie )
} else {
log .Error ("An error has occured, unable to generate token!" )
tokenString = jwtToken .Value
}
claims , err = fn (tokenString )
if err != nil {
return respondWithError (401 , "invalid token" , c )
}
} else {
next = next + "?token=" + token
}
return c .Redirect (redirectStatusCode , next )
in := authorization.AuthorizeRequest {}
in .Method = c .Request ().Method
in .Url = c .Request ().URL .Path
for _ , id := range claims .Permissions {
in .PermissionIds = append (in .PermissionIds , id )
}
if _ , err := authorizationClient .Authorize (context .Background (), & in ); err != nil {
return respondWithError (401 , "user not authorized" , c )
}
ctx := metadata .NewContext (
context .Background (),
metadata .Pairs ("author_id" , claims .Id , "author_name" , claims .Name , "author_email" , claims .Email ),
)
c .Set ("claims" , claims )
c .Set ("context" , ctx )
c .Set ("user_id" , claims .Id )
return next (c )
}
func redirectWithError (message string , to string , c echo.Context , err error ) error {
log .Error (message , err )
to = to + "?error=" + url .QueryEscape (message )
return c .Redirect (redirectStatusCode , to )
func respondWithError (code int , message string , c echo.Context ) error {
return c .JSON (code , echo.Map {"error" : message })
}
func googleAuthConfig (keyPath string ) * oauth2.Config {
jsonKey , err := ioutil .ReadFile (keyPath )
if err != nil {
log .Error (err )
}
conf , err := google .ConfigFromJSON (jsonKey , "email" )
if err != nil {
log .Error (err )
}
conf .Scopes = []string {
"https://www.googleapis.com/auth/userinfo.profile" ,
"https://www.googleapis.com/auth/userinfo.email" ,
func getTokenFromRequest (req * http.Request ) string {
authStr := req .Header .Get ("Authorization" )
if ! strings .HasPrefix (authStr , "Bearer " ) {
return ""
}
conf .RedirectURL = os .Getenv ("HOST_NAME" ) + "/auth/google/callback"
return conf
}
func generateToken (user * users.User ) (string , error ) {
// Create the token
user , err := usersClient .FindOrCreateUser (context .Background (), user )
if err != nil {
logger .Error ("Tried to get user" , "method" , "GenerateToken" , "message" , err )
return "" , err
}
claims := NewClaims (user )
claims .Permissions , err = getPermission (user .Roles )
if err != nil {
logger .Error ("Tried to get users permissions" , "method" , "GenerateToken" , "message" , err )
return "" , err
}
token := jwt .NewWithClaims (jwt .GetSigningMethod ("RS256" ), jwt.MapClaims {
"UserInfo" : claims ,
"exp" : time .Now ().Add (time .Hour * 24 * 3 ).Unix (),
})
// Sign and get the complete encoded token as a string
tokenString , err := token .SignedString (signKey )
if err != nil {
logger .Error ("Tried signing key" , "method" , "GenerateToken" , "message" , err )
return "" , err
}
return tokenString , nil
return authStr [7 :]
}
func getPermission (roles []* users.Role ) (map [string ]string , error ) {
var ids []string
for _ , role := range roles {
ids = append (ids , role .Id )
}
rolesID := authorization.RolesID {Ids : ids }
list , err := authorizationClient .FetchPermissions (context .Background (), & rolesID )
if err != nil || list .Values == nil {
return map [string ]string {}, err
}
return list .Values , err
}