Skip to content

Instantly share code, notes, and snippets.

@juancarloscruzd
Forked from codesword/authentication.go
Created March 30, 2018 19:10
Show Gist options
  • Select an option

  • Save juancarloscruzd/2cdea974134326491197cbd141d5002e to your computer and use it in GitHub Desktop.

Select an option

Save juancarloscruzd/2cdea974134326491197cbd141d5002e to your computer and use it in GitHub Desktop.

Revisions

  1. Ikem Okonkwo revised this gist Apr 22, 2017. 1 changed file with 65 additions and 189 deletions.
    254 changes: 65 additions & 189 deletions authentication.go
    Original file line number Diff line number Diff line change
    @@ -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
    }
  2. Ikem Okonkwo renamed this gist Jan 22, 2017. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. Ikem Okonkwo created this gist Jan 22, 2017.
    216 changes: 216 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,216 @@
    package auth

    import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/url"
    "os"
    "regexp"
    "time"

    "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="
    )

    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
    )

    func GoogleAuthFromConfig(keyPath string) 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:
    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 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 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)
    }

    // 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),
    }
    c.SetCookie(cookie)
    } else {
    log.Error("An error has occured, unable to generate token!")
    }
    } else {
    next = next + "?token=" + token
    }
    return c.Redirect(redirectStatusCode, next)
    }

    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 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",
    }

    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
    }

    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
    }