Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added github OAuth endpoints #72

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3dd7b53
feat: added jwt packages
Bikram-ghuku Jun 13, 2024
38c5389
feat: added basic github OAuth
Bikram-ghuku Jun 13, 2024
9578023
feat: added new env variables
Bikram-ghuku Jun 13, 2024
6d3ceaa
feat: added checks for github org team member
Bikram-ghuku Jun 13, 2024
52d671b
feat: removed slug conversion
Bikram-ghuku Jun 13, 2024
2441517
feat: updated template env to include org name and team name
Bikram-ghuku Jun 13, 2024
da917a4
feat: added comments for better understanding
Bikram-ghuku Jun 13, 2024
c8b458f
chore: added comments for better understanding
Bikram-ghuku Jun 13, 2024
bb153cd
feat: added JWT MiddleWare
Bikram-ghuku Jun 14, 2024
ed3ac4d
feat: added demo protected route
Bikram-ghuku Jun 14, 2024
c8f1028
fix: token in jwt parser
Bikram-ghuku Jun 14, 2024
a041e0e
chore: suggested changes
Bikram-ghuku Jun 14, 2024
c57baf5
feat: check if username is not nil
Bikram-ghuku Jun 14, 2024
0580b8b
feat: sending response as json object
Bikram-ghuku Jun 15, 2024
94729e5
Update backend/main.go
Bikram-ghuku Jun 15, 2024
66fedae
chore: commented the protected route for future reference
Bikram-ghuku Jun 15, 2024
cb7e257
Update backend/.env.template
Bikram-ghuku Jun 15, 2024
2424a1a
feat: suggested changes
Bikram-ghuku Jun 15, 2024
fbe5270
feat: comment protected route
Bikram-ghuku Jun 15, 2024
e047b83
feat: change from reading env to using env variable
Bikram-ghuku Jun 15, 2024
f875bc4
chore: update env var from TOKEN to JWT_TOKEN
Bikram-ghuku Jun 15, 2024
80c3960
feat: recommended changes
Bikram-ghuku Jun 15, 2024
9833a55
feat: removed unwanted comment
Bikram-ghuku Jun 15, 2024
fa29207
feat: fix variable name, update error message on empty ghcode
Bikram-ghuku Jun 15, 2024
e4b403b
feat: added checks for environment variables
Bikram-ghuku Jun 15, 2024
5398790
feat: added function for loading env
Bikram-ghuku Jun 24, 2024
22ba9fd
chore(vars): rename vars
Bikram-ghuku Jun 24, 2024
69fb3db
feat: update to go 1.22.4
Bikram-ghuku Jun 26, 2024
ba1d41a
refactor: 1.22 http handle func
Bikram-ghuku Jun 26, 2024
8a89139
feat: updated docker compose file
Bikram-ghuku Jun 26, 2024
77a77e9
feat: better error handling
Bikram-ghuku Jul 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ DB_USER=
DB_PASSWORD=
STATIC_FILES_STORAGE_LOCATION=/srv/static
UPLOADED_QPS_PATH=iqps/uploaded # Relative to `STATIC_FILES_STORAGE_LOCATION`. Final upload location will be /srv/static/iqps/uploaded
MAX_UPLOAD_LIMIT=10
MAX_UPLOAD_LIMIT=10
GH_CLIENT_ID= # public token of the oauth app
GH_PRIVATE_ID= # Private token of the oauth app
TOKEN= # JWT encryption token
harshkhandeparkar marked this conversation as resolved.
Show resolved Hide resolved
ORG_NAME= # name of the org
ORG_TEAM_SLUG= #URL friendly team Name
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ require (
)

require github.com/lib/pq v1.10.9

require github.com/golang-jwt/jwt/v5 v5.2.1
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
Expand Down
205 changes: 203 additions & 2 deletions backend/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"database/sql"
"encoding/json"
"errors"
Expand All @@ -14,12 +15,16 @@ import (
"strings"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv"
"github.com/rs/cors"

_ "github.com/lib/pq"
"github.com/rs/cors"
)

type contextKey string

const claimsKey = contextKey("claims")

type QuestionPaper struct {
ID int `json:"id"`
CourseCode string `json:"course_code"`
Expand Down Expand Up @@ -308,6 +313,200 @@ func populateDB(filename string) error {
return nil
}

func GhAuth(w http.ResponseWriter, r *http.Request) {
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
if r.Method != "POST" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
}

type BodyReg struct {
GhCode string `json:"code"`
}

type GithubAccessTokenResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}

type GithubUserResponse struct {
Login string `json:"login"`
ID int `json:"id"`
}

bodyReg := BodyReg{}
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
if err := json.NewDecoder(r.Body).Decode(&bodyReg); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if bodyReg.GhCode == "" {
http.Error(w, "Code cannot be empty", http.StatusBadRequest)
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
return
}
gh_pubKey := os.Getenv("GH_CLIENT_ID")
gh_pvtKey := os.Getenv("GH_PRIVATE_ID")
jwt_key := os.Getenv("TOKEN")
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved

// Get the access token for authenticating other endpoints
uri := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", gh_pubKey, gh_pvtKey, bodyReg.GhCode)

req, _ := http.NewRequest("POST", uri, nil)
req.Header.Set("Accept", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()

// Decode the response
var tokenResponse GithubAccessTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Get the username of the user who made the request
req, _ = http.NewRequest("GET", "https://api.github.com/user", nil)
req.Header.Set("Authorization", "Bearer "+tokenResponse.AccessToken)

resp, err = client.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()

// Decode the response
var userResponse GithubUserResponse
if err := json.NewDecoder(resp.Body).Decode(&userResponse); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

uname := userResponse.Login
// check if uname is empty
if uname == "" {
http.Error(w, "No user found", http.StatusUnauthorized)
return
}

// Get check parameters
org_name := os.Getenv("ORG_NAME")
org_team := os.Getenv("ORG_TEAM_SLUG")

// Send request to check status of the user in the given org's team
url := fmt.Sprintf("https://api.github.com/orgs/%s/teams/%s/memberships/%s", org_name, org_team, uname)
req, _ = http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+tokenResponse.AccessToken)
resp, err = client.Do(req)

var checkResp struct {
State string `json:"state"`
}

if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

defer resp.Body.Close()
//decode the response
if err := json.NewDecoder(resp.Body).Decode(&checkResp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Check if user is present in the team
if checkResp.State != "active" {
http.Error(w, "User is not authenticated", http.StatusUnauthorized)
return
}

// Create the response JWT
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"name": uname,
harshkhandeparkar marked this conversation as resolved.
Show resolved Hide resolved
})

tokenString, err := token.SignedString([]byte(jwt_key))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

http.Header.Add(w.Header(), "content-type", "application/json")

// Send the response
err = json.NewEncoder(w).Encode(&tokenString)
harshkhandeparkar marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

func JWTMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// get the authorisation header
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, "Missing authorization header")
return
}
JWTtoken := strings.Split(tokenString, " ")

if len(JWTtoken) != 2 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "Authorisation head is of inccorect type")
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
return
}
//parse the token
token, err := jwt.Parse(JWTtoken[1], func(t *jwt.Token) (interface{}, error) {
if _, OK := t.Method.(*jwt.SigningMethodHMAC); !OK {
return nil, errors.New("bad signed method received")
}

return []byte(os.Getenv("TOKEN")), nil
})

// Check if error in parsing jwt token
if err != nil {
http.Error(w, "Bad JWT token", http.StatusUnauthorized)
return
}
// Get the claims
claims, ok := token.Claims.(jwt.MapClaims)

if ok && token.Valid {
// If valid claims found, send response
ctx := context.WithValue(r.Context(), claimsKey, claims)
handler.ServeHTTP(w, r.WithContext(ctx))
} else {
fmt.Printf("Invalid JWT Token")
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
}
})
}

func getClaims(r *http.Request) jwt.MapClaims {
if claims, ok := r.Context().Value(claimsKey).(jwt.MapClaims); ok {
return claims
}
return nil
}

func protectedRoute(w http.ResponseWriter, r *http.Request) {
claims := getClaims(r)

if claims != nil {
fmt.Fprintf(w, "Hello, %s", claims["name"])
} else {
http.Error(w, "No claims found", http.StatusUnauthorized)
}
}

func CheckError(err error) {
if err != nil {
panic(err)
Expand Down Expand Up @@ -351,6 +550,8 @@ func main() {
http.HandleFunc("/year", year)
http.HandleFunc("/library", library)
http.HandleFunc("/upload", upload)
http.HandleFunc("/ghreg", GhAuth)
harshkhandeparkar marked this conversation as resolved.
Show resolved Hide resolved
http.Handle("/protected", JWTMiddleware(http.HandlerFunc(protectedRoute)))
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved

c := cors.New(cors.Options{
AllowedOrigins: []string{"https://qp.metakgp.org", "http://localhost:3000"},
Expand Down