Skip to content

Commit 41633d4

Browse files
feat(GIST-18): implement authentication in backend (#5)
* chore(GIST-18): add goth for authentication * feat(GIST-18): add tokens,users,users_auth & users_token tables to migrations * feat(GIST-18): add handlers for social auth github and gmail * wip(GIST-18): added registration + login on github and google * wip(GIST-18): added local auth * wip(GIST-18): added auth middleware in front of gists domain * docs(GIST-18): added docs for users
1 parent b03d2f6 commit 41633d4

22 files changed

+966
-47
lines changed

auth/auth_identity_model.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package auth
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
7+
"github.com/gistapp/api/storage"
8+
"github.com/gistapp/api/user"
9+
"github.com/gofiber/fiber/v2/log"
10+
)
11+
12+
type AuthIdentitySQL struct {
13+
ID sql.NullInt32
14+
Data sql.NullString
15+
Type sql.NullString
16+
ProviderID sql.NullString
17+
OwnerID sql.NullString
18+
}
19+
20+
type AuthIdentity struct {
21+
ID string `json:"id"`
22+
Data string `json:"data"`
23+
Owner string `json:"owner"`
24+
Type string `json:"type"`
25+
ProviderID string `json:"provider_id"`
26+
}
27+
28+
type JWTClaim struct {
29+
Pub string `json:"pub"`
30+
Email string `json:"email"`
31+
}
32+
33+
type AuthIdentityAndUser struct {
34+
AuthIdentity AuthIdentity
35+
User user.User
36+
}
37+
38+
type AuthIdentityModel interface {
39+
Save() (*AuthIdentity, error)
40+
}
41+
42+
func (ai *AuthIdentitySQL) Save() (*AuthIdentity, error) {
43+
row, err := storage.Database.Query("INSERT INTO auth_identity(data, type, owner_id, provider_id) VALUES ($1, $2, $3, $4) RETURNING auth_id, data, owner_id, type, provider_id", ai.Data.String, ai.Type.String, ai.OwnerID.String, ai.ProviderID.String)
44+
if err != nil {
45+
log.Error(err)
46+
return nil, errors.New("couldn't create auth identity")
47+
}
48+
var authIdentity AuthIdentity
49+
row.Next()
50+
err = row.Scan(&authIdentity.ID, &authIdentity.Data, &authIdentity.Owner, &authIdentity.Type, &authIdentity.ProviderID)
51+
if err != nil {
52+
log.Error(err)
53+
return nil, errors.New("couldn't find auth identity")
54+
}
55+
return &authIdentity, nil
56+
}
57+
58+
func (ai *AuthIdentitySQL) GetWithUser(provider_id string) (*AuthIdentityAndUser, error) {
59+
query := "SELECT a.auth_id, a.data, a.owner_id, a.type, a.provider_id, u.user_id, u.email, u.name, u.picture FROM auth_identity a JOIN users u ON a.owner_id = u.user_id WHERE a.provider_id = $1"
60+
61+
row, err := storage.Database.Query(query, provider_id)
62+
63+
if err != nil {
64+
log.Error(err)
65+
return nil, errors.New("couldn't get auth identity")
66+
}
67+
68+
row.Next()
69+
var authIdentity AuthIdentityAndUser
70+
err = row.Scan(&authIdentity.AuthIdentity.ID, &authIdentity.AuthIdentity.Data, &authIdentity.AuthIdentity.Owner, &authIdentity.AuthIdentity.Type, &authIdentity.AuthIdentity.ProviderID, &authIdentity.User.ID, &authIdentity.User.Email, &authIdentity.User.Name, &authIdentity.User.Picture)
71+
72+
if err != nil {
73+
log.Error(err)
74+
return nil, errors.New("couldn't find auth identity")
75+
}
76+
77+
return &authIdentity, nil
78+
79+
}

auth/controller.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package auth
2+
3+
import (
4+
"github.com/gofiber/fiber/v2"
5+
)
6+
7+
type AuthControllerImpl struct{}
8+
9+
type AuthLocalValidator struct {
10+
Email string `json:"email"`
11+
}
12+
13+
type AuthLocalVerificationValidator struct {
14+
Token string `json:"token"`
15+
Email string `json:"email"`
16+
}
17+
18+
func (a *AuthControllerImpl) Callback() fiber.Handler {
19+
return func(c *fiber.Ctx) error {
20+
return AuthService.Callback(c)
21+
}
22+
}
23+
24+
func (a *AuthControllerImpl) Authenticate() fiber.Handler {
25+
return func(c *fiber.Ctx) error {
26+
return AuthService.Authenticate(c)
27+
}
28+
}
29+
30+
func (a *AuthControllerImpl) LocalAuth() fiber.Handler {
31+
return func(c *fiber.Ctx) error {
32+
e := new(AuthLocalValidator)
33+
if err := c.BodyParser(e); err != nil {
34+
return c.Status(400).SendString("Request must be valid JSON with field email as text")
35+
}
36+
37+
if err := AuthService.LocalAuth(e.Email); err != nil {
38+
return c.Status(400).SendString(err.Error())
39+
}
40+
41+
return c.JSON(fiber.Map{"message": "Check your email for the token"})
42+
}
43+
}
44+
45+
func (a *AuthControllerImpl) VerifyAuthToken() fiber.Handler {
46+
return func(c *fiber.Ctx) error {
47+
e := new(AuthLocalVerificationValidator)
48+
49+
if err := c.BodyParser(e); err != nil {
50+
return c.Status(400).SendString("Request must be valid JSON with fields token and email as text")
51+
}
52+
53+
token := e.Token
54+
email := e.Email
55+
56+
jwt_token, err := AuthService.VerifyLocalAuthToken(token, email)
57+
58+
if err != nil {
59+
return c.Status(400).SendString(err.Error())
60+
}
61+
62+
return c.JSON(fiber.Map{"token": jwt_token})
63+
}
64+
}
65+
66+
var AuthController AuthControllerImpl = AuthControllerImpl{}

auth/router.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package auth
2+
3+
import "github.com/gofiber/fiber/v2"
4+
5+
type AuthRouter struct {
6+
Controller AuthControllerImpl
7+
}
8+
9+
func (r *AuthRouter) SubscribeRoutes(app *fiber.Router) {
10+
(*app).Get("/auth/callback/:provider", r.Controller.Callback())
11+
(*app).Get("/auth/:provider", r.Controller.Authenticate())
12+
(*app).Post("/auth/local/begin", r.Controller.LocalAuth())
13+
(*app).Post("/auth/local/verify", r.Controller.VerifyAuthToken())
14+
}

auth/service.go

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package auth
2+
3+
import (
4+
"database/sql"
5+
"encoding/json"
6+
"errors"
7+
"strings"
8+
9+
"github.com/gistapp/api/user"
10+
"github.com/gistapp/api/utils"
11+
"github.com/gofiber/fiber/v2"
12+
"github.com/gofiber/fiber/v2/log"
13+
"github.com/markbates/goth"
14+
"github.com/markbates/goth/providers/github"
15+
"github.com/markbates/goth/providers/google"
16+
"github.com/shareed2k/goth_fiber"
17+
)
18+
19+
type AuthServiceImpl struct{}
20+
21+
func (a *AuthServiceImpl) Authenticate(c *fiber.Ctx) error {
22+
if user, err := goth_fiber.CompleteUserAuth(c); err == nil {
23+
log.Info(user)
24+
return nil
25+
} else {
26+
return goth_fiber.BeginAuthHandler(c)
27+
}
28+
}
29+
30+
// generates a token and sends it to the user by email
31+
func (a *AuthServiceImpl) LocalAuth(email string) error {
32+
token_val := utils.GenToken(6)
33+
token_model := TokenSQL{
34+
Keyword: sql.NullString{String: email, Valid: true},
35+
Value: sql.NullString{String: token_val, Valid: true},
36+
Type: sql.NullString{String: string(LocalAuth), Valid: true},
37+
}
38+
39+
_, err := token_model.Save()
40+
41+
if err != nil {
42+
return err
43+
}
44+
45+
err = utils.SendEmail("Gistapp: Local Auth", "Your token is: "+token_val, email)
46+
47+
return err
48+
}
49+
50+
// verifies the token and finishes the registration
51+
func (a *AuthServiceImpl) VerifyLocalAuthToken(token string, email string) (string, error) {
52+
token_model := TokenSQL{
53+
Value: sql.NullString{String: token, Valid: true},
54+
Keyword: sql.NullString{String: email, Valid: true},
55+
Type: sql.NullString{String: string(LocalAuth), Valid: true},
56+
}
57+
token_data, err := token_model.Get()
58+
if err != nil {
59+
return "", err
60+
}
61+
err = token_data.Delete()
62+
if err != nil {
63+
return "", errors.New("couldn't invalidate token")
64+
}
65+
66+
//now we finish users registration
67+
goth_user := goth.User{
68+
UserID: email,
69+
Name: strings.Split(email, "@")[0],
70+
Email: email,
71+
AvatarURL: "https://vercel.com/api/www/avatar/?u=" + email + "&s=80",
72+
}
73+
74+
if user, _, err := a.GetUser(goth_user); err == nil {
75+
jwt_token, err := utils.CreateToken(user.Email, user.ID)
76+
if err != nil {
77+
return "", err
78+
}
79+
return jwt_token, nil
80+
}
81+
82+
user, err := a.Register(goth_user)
83+
84+
if err != nil {
85+
return "", err
86+
}
87+
88+
jwt_token, err := utils.CreateToken(user.Email, user.ID)
89+
90+
return jwt_token, err
91+
}
92+
93+
func (a *AuthServiceImpl) Callback(c *fiber.Ctx) error {
94+
auth_user, err := goth_fiber.CompleteUserAuth(c)
95+
if err != nil {
96+
log.Error(err)
97+
return ErrCantCompleteAuth
98+
}
99+
100+
user_md, _, err := a.GetUser(auth_user)
101+
102+
if err == nil {
103+
token, err := utils.CreateToken(user_md.Email, user_md.ID)
104+
if err != nil {
105+
return err
106+
}
107+
return c.JSON(fiber.Map{
108+
"token": token,
109+
})
110+
}
111+
112+
user_md, err = a.Register(auth_user)
113+
114+
if err != nil {
115+
return err
116+
}
117+
118+
jwt, err := utils.CreateToken(user_md.Email, user_md.ID)
119+
if err != nil {
120+
return err
121+
}
122+
123+
return c.JSON(fiber.Map{
124+
"token": jwt,
125+
})
126+
}
127+
128+
func (a *AuthServiceImpl) GetUser(auth_user goth.User) (*user.User, *AuthIdentity, error) {
129+
auth_and_user, err := new(AuthIdentitySQL).GetWithUser(auth_user.UserID)
130+
if err != nil {
131+
return nil, nil, err
132+
}
133+
134+
return &auth_and_user.User, &auth_and_user.AuthIdentity, nil
135+
}
136+
137+
func (a *AuthServiceImpl) Register(auth_user goth.User) (*user.User, error) {
138+
data, err := json.Marshal(auth_user)
139+
if err != nil {
140+
return nil, errors.New("couldn't marshal user")
141+
}
142+
143+
user_model := user.UserSQL{
144+
ID: sql.NullString{String: auth_user.UserID, Valid: true},
145+
Email: sql.NullString{String: auth_user.Email, Valid: true},
146+
Name: sql.NullString{String: auth_user.Name, Valid: true},
147+
Picture: sql.NullString{String: auth_user.AvatarURL, Valid: true},
148+
}
149+
150+
user_data, err := user_model.Save()
151+
152+
if err != nil {
153+
return nil, err
154+
}
155+
156+
auth_identity_model := AuthIdentitySQL{
157+
Data: sql.NullString{String: string(data), Valid: true},
158+
Type: sql.NullString{String: auth_user.Provider, Valid: true},
159+
OwnerID: sql.NullString{String: user_data.ID, Valid: true},
160+
ProviderID: sql.NullString{String: auth_user.UserID, Valid: true},
161+
}
162+
163+
auth_identity, err := auth_identity_model.Save()
164+
log.Info(auth_identity)
165+
return user_data, err
166+
}
167+
168+
func (a *AuthServiceImpl) RegisterProviders() {
169+
goth.UseProviders(
170+
google.New(utils.Get("GOOGLE_KEY"), utils.Get("GOOGLE_SECRET"), utils.Get("PUBLIC_URL")+"/auth/callback/google"),
171+
github.New(utils.Get("GITHUB_KEY"), utils.Get("GITHUB_SECRET"), utils.Get("PUBLIC_URL")+"/auth/callback/github"),
172+
)
173+
}
174+
175+
func (a *AuthServiceImpl) IsAuthenticated(token string) (*JWTClaim, error) {
176+
claims, err := utils.VerifyJWT(token)
177+
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
jwtClaim := new(JWTClaim)
183+
jwtClaim.Pub = claims["pub"].(string)
184+
jwtClaim.Email = claims["email"].(string)
185+
186+
return jwtClaim, nil
187+
}
188+
189+
var AuthService AuthServiceImpl = AuthServiceImpl{}
190+
var ErrCantCompleteAuth = errors.New("can't complete auth")

0 commit comments

Comments
 (0)