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 all 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
JWT_SECRET= # JWT encryption secret
GH_ORG_NAME= # name of the org
GH_ORG_TEAM_SLUG= #URL friendly team Name
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.21 AS builder
FROM golang:1.22.4 AS builder

WORKDIR /src
COPY . .
Expand Down
5 changes: 5 additions & 0 deletions backend/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ services:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- UPLOADED_QPS_PATH=${UPLOADED_QPS_PATH}
- GH_CLIENT_ID=${GH_CLIENT_ID}
- GH_PRIVATE_ID=${GH_PRIVATE_ID}
- JWT_SECRET=${JWT_SECRET}
- GH_ORG_NAME=${GH_ORG_NAME}
- GH_ORG_TEAM_SLUG=${GH_ORG_TEAM_SLUG}

networks:
metaploy-network:
Expand Down
4 changes: 3 additions & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
module github.com/metakgp/iqps/backend

go 1.21.6
go 1.22.4

require (
github.com/joho/godotenv v1.5.1
github.com/rs/cors v1.10.1
)

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
246 changes: 239 additions & 7 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 All @@ -44,8 +49,32 @@ var (
staticFilesUrl string
staticFilesStorageLocation string
uploadedQpsPath string
gh_pubKey string
gh_pvtKey string
jwt_secret string
org_name string
org_team string
)

type GhOAuthReqBody 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"`
}

var respData struct {
Token string `json:"token"`
}

const init_db = `CREATE TABLE IF NOT EXISTS qp (
id SERIAL PRIMARY KEY,
course_code TEXT NOT NULL DEFAULT '',
Expand Down Expand Up @@ -159,10 +188,6 @@ func search(w http.ResponseWriter, r *http.Request) {
}

func upload(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var response []uploadEndpointRes
// Max total size of 50MB
const MaxBodySize = 50 << 20 // 1<<20 = 1024*1024 = 1MB
Expand Down Expand Up @@ -308,12 +333,215 @@ 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

ghOAuthReqBody := GhOAuthReqBody{}
if err := json.NewDecoder(r.Body).Decode(&ghOAuthReqBody); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if ghOAuthReqBody.GhCode == "" {
http.Error(w, "Github OAuth Code cannot be empty", http.StatusBadRequest)
return
}

// 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, ghOAuthReqBody.GhCode)

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

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error Getting Github Access Token: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()

// Decode the response
var tokenResponse GithubAccessTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
fmt.Println("Error Decoding Github Access Token: ", err.Error())
http.Error(w, "Internal Server 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 {
fmt.Println("Error getting username: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()

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

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

// 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 {
fmt.Println("Error validating user membership: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

defer resp.Body.Close()
//decode the response
if err := json.NewDecoder(resp.Body).Decode(&checkResp); err != nil {
fmt.Println("Error decoding gh validation body: ", err.Error())
http.Error(w, "Internal Server 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{
"username": uname,
})

tokenString, err := token.SignedString([]byte(jwt_secret))
if err != nil {
fmt.Println("Error Sigining JWT: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

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

// Send the response

respData.Token = tokenString
err = json.NewEncoder(w).Encode(&respData)
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 incorrect type")
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(jwt_secret), 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 && claims["username"] != nil {
// If valid claims found, send response
ctx := context.WithValue(r.Context(), claimsKey, claims)
handler.ServeHTTP(w, r.WithContext(ctx))
} else {
http.Error(w, "Invalid JWT token", http.StatusUnauthorized)
}
})
}

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["username"])
// } else {
// http.Error(w, "No claims found", http.StatusUnauthorized)
// }
// }

func CheckError(err error) {
if err != nil {
panic(err)
}
}

func LoadGhEnv() {
gh_pubKey = os.Getenv("GH_CLIENT_ID")
gh_pvtKey = os.Getenv("GH_PRIVATE_ID")
org_name = os.Getenv("GH_ORG_NAME")
org_team = os.Getenv("GH_ORG_TEAM_SLUG")

jwt_secret = os.Getenv("JWT_SECRET")

if gh_pubKey == "" {
panic("Client id for Github OAuth cannot be empty")
}
Bikram-ghuku marked this conversation as resolved.
Show resolved Hide resolved
if gh_pvtKey == "" {
panic("Client Private Key for Github OAuth cannot be empty")
}
if org_name == "" {
panic("Organisation name cannot be empty")
}
if org_team == "" {
panic("Team name of the Organistion cannot be empty")
}
if jwt_secret == "" {
panic("JWT Secret Key cannot be empty")
}
}

func main() {
err := godotenv.Load(".env")
if err != nil {
Expand All @@ -324,6 +552,8 @@ func main() {
port, err := strconv.Atoi(os.Getenv("DB_PORT"))
CheckError(err)

LoadGhEnv()

user := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
dbname := os.Getenv("DB_NAME")
Expand All @@ -350,7 +580,9 @@ func main() {
http.HandleFunc("/search", search)
http.HandleFunc("/year", year)
http.HandleFunc("/library", library)
http.HandleFunc("/upload", upload)
http.HandleFunc("POST /upload", upload)
http.HandleFunc("GET /oauth", GhAuth)
//http.Handle("/protected", JWTMiddleware(http.HandlerFunc(protectedRoute)))

c := cors.New(cors.Options{
AllowedOrigins: []string{"https://qp.metakgp.org", "http://localhost:3000"},
Expand Down
2 changes: 1 addition & 1 deletion go.work
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
go 1.21.6
go 1.22.4

use (
./backend
Expand Down