From dcd5131582ea79e928e7f424345153d9ab9a7be0 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Thu, 23 Sep 2021 11:42:14 +0200 Subject: [PATCH 01/15] Start working --- pkg/models/oauth_client.go | 13 +++++++++++++ pkg/models/user.go | 2 +- pkg/oauth2/authorize.go | 21 +++++++++++++++++++++ pkg/web/types.go | 9 +++++++++ web/template/authorize.html | 7 +++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 pkg/models/oauth_client.go create mode 100644 pkg/oauth2/authorize.go create mode 100644 web/template/authorize.html diff --git a/pkg/models/oauth_client.go b/pkg/models/oauth_client.go new file mode 100644 index 0000000..d3f2053 --- /dev/null +++ b/pkg/models/oauth_client.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type OauthClient struct { + ID uint `json:"id" gorm:"primaryKey;type:bigint(20);unsigned;column:id"` + UserID int `json:"user_id" gorm:"primaryKey;type:bigint(20);unsigned;column:user_id"` + Name string `json:"name" gorm:"type:varchar(255);column:name"` + Secret string `json:"-" gorm:"type:varchar(100);column:secret"` + + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` +} diff --git a/pkg/models/user.go b/pkg/models/user.go index 94a4207..6e1a6f4 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -8,7 +8,7 @@ type User struct { ID uint `json:"id" gorm:"primaryKey;type:bigint(20);unsigned;column:id"` NameFirst string `json:"name_first" gorm:"type:varchar(255);column:name_first"` NameLast string `json:"name_last" gorm:"type:varchar(255);column:name_last"` - Email string `json:"-" gorm:"type:varchar(255);column:email"` + Email string `json:"email" gorm:"type:varchar(255);column:email"` Rating int `json:"rating" gorm:"type:tinyint(4);column:rating"` PilotRating int `json:"pilot_rating" gorm:"type:tinyint(4);column:pilot_rating"` CountryID string `json:"country_id" gorm:"type:varchar(255);column:country_id"` diff --git a/pkg/oauth2/authorize.go b/pkg/oauth2/authorize.go new file mode 100644 index 0000000..6e7a6ca --- /dev/null +++ b/pkg/oauth2/authorize.go @@ -0,0 +1,21 @@ +package oauth2 + +import ( + "auth/pkg/response" + "log" + "net/http" + "text/template" +) + +func Authorize(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFiles("web/template/authorize.html") + + if err != nil { + log.Println("Error occurred while parsing the file. Error:", err.Error()) + res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) + res.Process() + return + } + + tmpl.Execute(w, nil) +} diff --git a/pkg/web/types.go b/pkg/web/types.go index 3110156..ebb2083 100644 --- a/pkg/web/types.go +++ b/pkg/web/types.go @@ -87,6 +87,15 @@ func (server *Server) loadRoutes() { true, false, }, + { + "/app/authorize", + []string{ + "GET", + }, + oauth2.Authorize, + false, + true, + }, } } diff --git a/web/template/authorize.html b/web/template/authorize.html new file mode 100644 index 0000000..fce3201 --- /dev/null +++ b/web/template/authorize.html @@ -0,0 +1,7 @@ + + +

+ test +

+ + \ No newline at end of file From 88d7bfaca68628672bd497b66424c8d9411199bc Mon Sep 17 00:00:00 2001 From: Matan Budimir <36965687+MatanBudimir@users.noreply.github.com> Date: Mon, 27 Sep 2021 16:25:14 +0200 Subject: [PATCH 02/15] Update oauth_client.go --- pkg/models/oauth_client.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/models/oauth_client.go b/pkg/models/oauth_client.go index d3f2053..3cfb466 100644 --- a/pkg/models/oauth_client.go +++ b/pkg/models/oauth_client.go @@ -3,11 +3,15 @@ package models import "time" type OauthClient struct { - ID uint `json:"id" gorm:"primaryKey;type:bigint(20);unsigned;column:id"` - UserID int `json:"user_id" gorm:"primaryKey;type:bigint(20);unsigned;column:user_id"` - Name string `json:"name" gorm:"type:varchar(255);column:name"` - Secret string `json:"-" gorm:"type:varchar(100);column:secret"` - - CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` - UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` + ID uint `json:"-" gorm:"primaryKey;type:bigint(20);unsigned;column:id"` + UserID int `json:"user_id" gorm:"primaryKey;type:bigint(20);unsigned;column:user_id"` + Name string `json:"name" gorm:"type:varchar(255);column:name"` + Secret string `json:"-" gorm:"type:varchar(100);column:secret"` + Provider string `json:"provider" gorm:"type:varchar(255);column:provider"` + Redirect string `json:" redirect" gorm:"type:varchar(65535);column:redirect"` + PersonalAccessClient string `json:"personal_access_client" gorm:"type:boolean;column:personal_access_client"` + PasswordClient string `json:"password_client" gorm:"type:boolean;column:password_client"` + Revoked string `json:"revoked" gorm:"type:boolean;column:revoked"` + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` } From e215133865888d57d498da78cd750a6843853115 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 15:37:11 +0200 Subject: [PATCH 03/15] Start working on oauth2 --- Dockerfile | 10 ++ pkg/models/oauth_client.go | 11 +- pkg/oauth2/authorize.go | 13 ++- pkg/oauth2/types.go | 123 +++++++++++++++++++++ pkg/web/types.go | 3 +- swagger.json | 210 ++++++++++++++++++++++++++++++++++++ web/template/authorize.html | 7 -- 7 files changed, 359 insertions(+), 18 deletions(-) create mode 100644 Dockerfile create mode 100644 pkg/oauth2/types.go create mode 100644 swagger.json delete mode 100644 web/template/authorize.html diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b33be4a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.17-alpine + +WORKDIR ./ + +RUN go mod download + +RUN go build -o /api /cmd/api/main.go + +EXPOSE 3000 +ENTRYPOINT [ "/api/main" ] \ No newline at end of file diff --git a/pkg/models/oauth_client.go b/pkg/models/oauth_client.go index 3cfb466..ca0b5bc 100644 --- a/pkg/models/oauth_client.go +++ b/pkg/models/oauth_client.go @@ -7,11 +7,12 @@ type OauthClient struct { UserID int `json:"user_id" gorm:"primaryKey;type:bigint(20);unsigned;column:user_id"` Name string `json:"name" gorm:"type:varchar(255);column:name"` Secret string `json:"-" gorm:"type:varchar(100);column:secret"` - Provider string `json:"provider" gorm:"type:varchar(255);column:provider"` - Redirect string `json:" redirect" gorm:"type:varchar(65535);column:redirect"` - PersonalAccessClient string `json:"personal_access_client" gorm:"type:boolean;column:personal_access_client"` - PasswordClient string `json:"password_client" gorm:"type:boolean;column:password_client"` - Revoked string `json:"revoked" gorm:"type:boolean;column:revoked"` + Redirect string `json:"redirect" gorm:"type:varchar(200);column:redirect"` + Revoked bool `json:"revoked" gorm:"type:boolean;column:revoked"` CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` } + +func (client OauthClient) IsValidRedirectURI(uri string) bool { + return client.Redirect == uri +} diff --git a/pkg/oauth2/authorize.go b/pkg/oauth2/authorize.go index 6e7a6ca..3bf501c 100644 --- a/pkg/oauth2/authorize.go +++ b/pkg/oauth2/authorize.go @@ -1,21 +1,24 @@ package oauth2 import ( - "auth/pkg/response" + "api/pkg/response" + "fmt" "log" "net/http" - "text/template" ) func Authorize(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFiles("web/template/authorize.html") + req, err := newRequest(r) if err != nil { - log.Println("Error occurred while parsing the file. Error:", err.Error()) + log.Println("Error occurred while creating a new request. Error:", err.Error()) res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) res.Process() return } - tmpl.Execute(w, nil) + if err := req.Validate(); err != nil { + http.Redirect(w, r, fmt.Sprintf("%s?%s", r.Form.Get("redirect_uri"), err.Error()), http.StatusFound) + return + } } diff --git a/pkg/oauth2/types.go b/pkg/oauth2/types.go new file mode 100644 index 0000000..bd4ff8b --- /dev/null +++ b/pkg/oauth2/types.go @@ -0,0 +1,123 @@ +package oauth2 + +import ( + "api/internal/pkg/database" + "api/pkg/models" + "errors" + "net/http" + "net/url" + "strings" +) + +var scopes = []string{"full_name", "email", "vatsim_details", "country"} + +type authorizationRequest struct { + ResponseType, ClientID, RedirectURI, State string + Scopes []string +} + +type requestError struct { + Response url.Values +} + +func (err requestError) Error() string { + return err.Response.Encode() +} + +func newRequest(r *http.Request) (*authorizationRequest, error) { + if r == nil { + return nil, errors.New("please provide a valid request") + } + + if err := r.ParseForm(); err != nil { + return nil, err + } + + return &authorizationRequest{ + ResponseType: r.Form.Get("response_type"), + ClientID: r.Form.Get("client_id"), + RedirectURI: r.Form.Get("redirect_uri"), + Scopes: strings.Split(r.Form.Get("scope"), " "), + State: r.Form.Get("state"), + }, nil +} + +func (request *authorizationRequest) Validate() error { + if len(request.ClientID) < 1 || len(request.ResponseType) < 1 || len(request.RedirectURI) < 1 { + return requestError{ + Response: request.formatURL("invalid_request"), + } + } + + if request.ResponseType != "code" { + return requestError{Response: request.formatURL("unsupported_response_type")} + } + + // also ensure the first element is an empty string because strings.Split returns an empty string + if len(request.Scopes) > 0 && request.Scopes[0] != "" { + for _, scope := range request.Scopes { + if !request.isValidScope(scope) { + return requestError{Response: request.formatURL("invalid_scope")} + } + } + } else { + request.Scopes = scopes + } + + client := models.OauthClient{} + if err := database.DB.Where("id = ?", request.ClientID).First(&client).Error; err != nil { + return requestError{Response: request.formatURL("unauthorized_client")} + } + + if client.Revoked { + return requestError{Response: request.formatURL("unauthorized_client")} + } + + if !client.IsValidRedirectURI(request.RedirectURI) { + return requestError{Response: request.formatURL("unauthorized_client")} + } + + return nil +} + +func (request *authorizationRequest) formatURL(err string) url.Values { + val := url.Values{} + val.Set("error", err) + val.Set("error_description", request.getError(err)) + + if len(request.State) > 0 { + val.Set("state", request.State) + } + + return val +} + +func (request authorizationRequest) getError(err string) string { + authErrors := map[string]string{ + "invalid_request": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", + "unauthorized_client": "The client is not authorized to request an authorization code using this method.", + "access_denied": "The resource owner or authorization server denied the request.", + "unsupported_response_type": "The authorization server does not support obtaining an authorization code using this method.", + "invalid_scope": "The requested scope is invalid, unknown, or malformed.", + "server_error": "The authorization server encountered an unexpected condition that prevented it from fulfilling the request.", + "temporarily_unavailable": "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.", + } + + e, ok := authErrors[err] + + if !ok { + return "Error occurred." + } + + return e +} + +func (request authorizationRequest) isValidScope(scope string) bool { + for _, availableScope := range scopes { + if availableScope == scope { + return true + } + } + + return false +} diff --git a/pkg/web/types.go b/pkg/web/types.go index e1aaf12..8a14b50 100644 --- a/pkg/web/types.go +++ b/pkg/web/types.go @@ -249,13 +249,14 @@ func (server *Server) loadRoutes() { true, }, { - "/app/authorize", + "/oauth/authorize", []string{ "GET", }, oauth2.Authorize, false, true, + false, }, } } diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..60e3fe9 --- /dev/null +++ b/swagger.json @@ -0,0 +1,210 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "email": "apiteam@swagger.io", + "url": "http://swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/api", + "schemes": [ + "http" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/pets": { + "get": { + "description": "Returns all pets from the system that the user has access to\nNam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.\n\nSed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.\n", + "operationId": "findPets", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "type": "array", + "collectionFormat": "csv", + "items": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "parameters": [ + { + "name": "pet", + "in": "body", + "description": "Pet to add to the store", + "required": true, + "schema": { + "$ref": "#/definitions/NewPet" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/pets/{id}": { + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "find pet by id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + "definitions": { + "Pet": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/NewPet" + }, + { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + } + } + ] + }, + "NewPet": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/web/template/authorize.html b/web/template/authorize.html deleted file mode 100644 index fce3201..0000000 --- a/web/template/authorize.html +++ /dev/null @@ -1,7 +0,0 @@ - - -

- test -

- - \ No newline at end of file From 7f49a2268ed5937030d89a4a428f5059b58b8bba Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 16:55:27 +0200 Subject: [PATCH 04/15] Fixes --- go.mod | 1 + go.sum | 9 ++++++++- internal/app/api/start.go | 3 +++ pkg/models/oauth_auth_code.go | 13 +++++++++++++ pkg/models/oauth_client.go | 16 ++++++++-------- pkg/oauth2/authorize.go | 32 +++++++++++++++++++++++++++++++- pkg/oauth2/types.go | 29 +++++++++++++++-------------- 7 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 pkg/models/oauth_auth_code.go diff --git a/go.mod b/go.mod index 4cbca2b..ee3a199 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/joho/godotenv v1.3.0 github.com/labstack/gommon v0.3.0 + github.com/matoous/go-nanoid/v2 v2.0.0 golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac gorm.io/driver/mysql v1.1.2 gorm.io/gorm v1.21.15 diff --git a/go.sum b/go.sum index ab79839..c93d1e7 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,10 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= +github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= +github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -67,8 +71,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= @@ -135,6 +140,8 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M= gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM= gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= diff --git a/internal/app/api/start.go b/internal/app/api/start.go index ec15908..d596a1d 100644 --- a/internal/app/api/start.go +++ b/internal/app/api/start.go @@ -3,6 +3,7 @@ package api import ( "api/internal/pkg/database" "api/pkg/cache" + "api/pkg/models" "api/pkg/web" "github.com/joho/godotenv" "log" @@ -27,6 +28,8 @@ func Start() { database.Connect() cache.New() + database.DB.AutoMigrate(models.OauthAuthCode{}) + server := web.New() log.Println("Starting the web server") diff --git a/pkg/models/oauth_auth_code.go b/pkg/models/oauth_auth_code.go new file mode 100644 index 0000000..b09c266 --- /dev/null +++ b/pkg/models/oauth_auth_code.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type OauthAuthCode struct { + ID string `json:"-" gorm:"primaryKey;type:varchar(100);unsigned;column:id;unique"` + ClientID uint `json:"client_id" gorm:"type:bigint(20);unsigned;column:client_id"` + Client OauthClient + Scopes string `json:"-" gorm:"type:varchar(200);column:scopes"` + UserAgent string `json:"-" gorm:"type:varchar(200);column:user_agent"` + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` +} diff --git a/pkg/models/oauth_client.go b/pkg/models/oauth_client.go index ca0b5bc..c180006 100644 --- a/pkg/models/oauth_client.go +++ b/pkg/models/oauth_client.go @@ -3,14 +3,14 @@ package models import "time" type OauthClient struct { - ID uint `json:"-" gorm:"primaryKey;type:bigint(20);unsigned;column:id"` - UserID int `json:"user_id" gorm:"primaryKey;type:bigint(20);unsigned;column:user_id"` - Name string `json:"name" gorm:"type:varchar(255);column:name"` - Secret string `json:"-" gorm:"type:varchar(100);column:secret"` - Redirect string `json:"redirect" gorm:"type:varchar(200);column:redirect"` - Revoked bool `json:"revoked" gorm:"type:boolean;column:revoked"` - CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` - UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` + ID uint `json:"-" gorm:"primaryKey;type:bigint(20);unsigned;column:id"` + UserID int `json:"user_id" gorm:"primaryKey;type:bigint(20);unsigned;column:user_id"` + Name string `json:"name" gorm:"type:varchar(255);column:name"` + Secret string `json:"-" gorm:"type:varchar(100);column:secret"` + Redirect string `json:"redirect" gorm:"type:varchar(200);column:redirect"` + Revoked bool `json:"revoked" gorm:"type:boolean;column:revoked"` + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` } func (client OauthClient) IsValidRedirectURI(uri string) bool { diff --git a/pkg/oauth2/authorize.go b/pkg/oauth2/authorize.go index 3bf501c..59629b1 100644 --- a/pkg/oauth2/authorize.go +++ b/pkg/oauth2/authorize.go @@ -1,8 +1,11 @@ package oauth2 import ( + "api/internal/pkg/database" + "api/pkg/models" "api/pkg/response" "fmt" + "github.com/matoous/go-nanoid/v2" "log" "net/http" ) @@ -17,8 +20,35 @@ func Authorize(w http.ResponseWriter, r *http.Request) { return } - if err := req.Validate(); err != nil { + client, err := req.Validate() + + if err != nil { + log.Println(err.Error()) http.Redirect(w, r, fmt.Sprintf("%s?%s", r.Form.Get("redirect_uri"), err.Error()), http.StatusFound) return } + + id, err := gonanoid.New(100) + + if err != nil { + log.Println("Error occurred while generating the UID. Error:", err.Error()) + res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) + res.Process() + return + } + + login := models.OauthAuthCode{ + ID: id, + ClientID: client.ID, + Client: *client, + Scopes: req.Scopes, + UserAgent: r.UserAgent(), + } + + if err := database.DB.Create(&login).Error; err != nil { + log.Println("Error occurred while saving the auth code. Error:", err.Error()) + res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) + res.Process() + return + } } diff --git a/pkg/oauth2/types.go b/pkg/oauth2/types.go index bd4ff8b..eaccce3 100644 --- a/pkg/oauth2/types.go +++ b/pkg/oauth2/types.go @@ -12,8 +12,7 @@ import ( var scopes = []string{"full_name", "email", "vatsim_details", "country"} type authorizationRequest struct { - ResponseType, ClientID, RedirectURI, State string - Scopes []string + ResponseType, ClientID, RedirectURI, State, Scopes string } type requestError struct { @@ -37,47 +36,49 @@ func newRequest(r *http.Request) (*authorizationRequest, error) { ResponseType: r.Form.Get("response_type"), ClientID: r.Form.Get("client_id"), RedirectURI: r.Form.Get("redirect_uri"), - Scopes: strings.Split(r.Form.Get("scope"), " "), + Scopes: r.Form.Get("scope"), State: r.Form.Get("state"), }, nil } -func (request *authorizationRequest) Validate() error { +func (request *authorizationRequest) Validate() (*models.OauthClient, error) { if len(request.ClientID) < 1 || len(request.ResponseType) < 1 || len(request.RedirectURI) < 1 { - return requestError{ + return nil, requestError{ Response: request.formatURL("invalid_request"), } } if request.ResponseType != "code" { - return requestError{Response: request.formatURL("unsupported_response_type")} + return nil, requestError{Response: request.formatURL("unsupported_response_type")} } + s := strings.Split(request.Scopes, " ") + // also ensure the first element is an empty string because strings.Split returns an empty string - if len(request.Scopes) > 0 && request.Scopes[0] != "" { - for _, scope := range request.Scopes { + if len(s) > 0 && s[0] != "" { + for _, scope := range s { if !request.isValidScope(scope) { - return requestError{Response: request.formatURL("invalid_scope")} + return nil, requestError{Response: request.formatURL("invalid_scope")} } } } else { - request.Scopes = scopes + request.Scopes = strings.Join(s, " ") } client := models.OauthClient{} if err := database.DB.Where("id = ?", request.ClientID).First(&client).Error; err != nil { - return requestError{Response: request.formatURL("unauthorized_client")} + return nil, requestError{Response: request.formatURL("unauthorized_client")} } if client.Revoked { - return requestError{Response: request.formatURL("unauthorized_client")} + return nil, requestError{Response: request.formatURL("unauthorized_client")} } if !client.IsValidRedirectURI(request.RedirectURI) { - return requestError{Response: request.formatURL("unauthorized_client")} + return nil, requestError{Response: request.formatURL("unauthorized_client")} } - return nil + return &client, nil } func (request *authorizationRequest) formatURL(err string) url.Values { From 07e8f5338104430ae04be3d8cdf892a366492493 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 16:56:05 +0200 Subject: [PATCH 05/15] Remove auto migrate --- internal/app/api/start.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/app/api/start.go b/internal/app/api/start.go index d596a1d..ec15908 100644 --- a/internal/app/api/start.go +++ b/internal/app/api/start.go @@ -3,7 +3,6 @@ package api import ( "api/internal/pkg/database" "api/pkg/cache" - "api/pkg/models" "api/pkg/web" "github.com/joho/godotenv" "log" @@ -28,8 +27,6 @@ func Start() { database.Connect() cache.New() - database.DB.AutoMigrate(models.OauthAuthCode{}) - server := web.New() log.Println("Starting the web server") From 12d6b554762d5019f4537bb109285f3c5ab4a440 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 16:57:13 +0200 Subject: [PATCH 06/15] Use all scopes as default --- pkg/oauth2/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/oauth2/types.go b/pkg/oauth2/types.go index eaccce3..9a66d9e 100644 --- a/pkg/oauth2/types.go +++ b/pkg/oauth2/types.go @@ -62,7 +62,7 @@ func (request *authorizationRequest) Validate() (*models.OauthClient, error) { } } } else { - request.Scopes = strings.Join(s, " ") + request.Scopes = strings.Join(scopes, " ") } client := models.OauthClient{} From b23bb1bcec73e0786ed4e0082b2713b68a3b91b7 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 20:41:07 +0200 Subject: [PATCH 07/15] Save token to cookie and marshal JSON depending on scopes --- pkg/oauth2/authorize.go | 17 +++++++++++ pkg/oauth2/oauth.go | 56 +++++++++++++++++++++++++------------ pkg/vatsim/connect/types.go | 52 +++++++++++++++++----------------- 3 files changed, 81 insertions(+), 44 deletions(-) diff --git a/pkg/oauth2/authorize.go b/pkg/oauth2/authorize.go index 59629b1..abda8e0 100644 --- a/pkg/oauth2/authorize.go +++ b/pkg/oauth2/authorize.go @@ -4,10 +4,12 @@ import ( "api/internal/pkg/database" "api/pkg/models" "api/pkg/response" + "api/pkg/vatsim/connect" "fmt" "github.com/matoous/go-nanoid/v2" "log" "net/http" + "time" ) func Authorize(w http.ResponseWriter, r *http.Request) { @@ -51,4 +53,19 @@ func Authorize(w http.ResponseWriter, r *http.Request) { res.Process() return } + + cookie := http.Cookie{ + Name: "token", + Value: id, + Path: "/oauth", + Domain: "localhost", + Expires: time.Now().UTC().Add(time.Minute*5), + Secure: true, + HttpOnly: true, + SameSite: 1, + } + + http.SetCookie(w, &cookie) + + connect.Login(w, r) } diff --git a/pkg/oauth2/oauth.go b/pkg/oauth2/oauth.go index 36ebf62..eac812b 100644 --- a/pkg/oauth2/oauth.go +++ b/pkg/oauth2/oauth.go @@ -30,7 +30,7 @@ func User(w http.ResponseWriter, r *http.Request) { return } - bytes, err := connectJson(user) + bytes, err := connectJson(user, []string{"full_name"}) if err != nil { log.Printf("Error occurred while marshalling the response on /api/user. Error: %s.", err.Error()) @@ -47,20 +47,30 @@ func User(w http.ResponseWriter, r *http.Request) { } } -func connectJson(user models.User) ([]byte, error) { - res := connect.UserData{Data: connect.Data{ - CID: fmt.Sprintf("%d", user.ID), - Personal: connect.Personal{ - NameFirst: user.NameFirst, - NameLast: user.NameLast, - NameFull: fmt.Sprintf("%s %s", user.NameFirst, user.NameLast), - Email: user.Email, - Country: connect.Country{ - ID: user.CountryID, - Name: user.CountryName, - }, - }, - Vatsim: connect.Vatsim{ +func connectJson(user models.User, scopes []string) ([]byte, error) { + cUser := connect.UserData{} + + cUser.Data.CID = fmt.Sprintf("%d", user.ID) + + if isInScopes(scopes, "full_name") { + cUser.Data.Personal.NameFirst = user.NameFirst + cUser.Data.Personal.NameLast = user.NameLast + cUser.Data.Personal.NameFull = fmt.Sprintf("%s %s", user.NameFirst, user.NameLast) + } + + if isInScopes(scopes, "country") { + cUser.Data.Personal.Country = connect.Country{ + ID: user.CountryID, + Name: user.CountryName, + } + } + + if isInScopes(scopes, "email") { + cUser.Data.Personal.Email = user.Email + } + + if isInScopes(scopes, "vatsim_details") { + cUser.Data.Vatsim = connect.Vatsim{ Rating: connect.Rating{ ID: user.Rating, }, @@ -79,8 +89,18 @@ func connectJson(user models.User) ([]byte, error) { ID: user.SubdivisionID, Name: user.SubdivisionName, }, - }, - }} + } + } + + return json.Marshal(cUser) +} + +func isInScopes(scopes []string, scope string) bool { + for _, s := range scopes { + if scope == s { + return true + } + } - return json.Marshal(res) + return false } diff --git a/pkg/vatsim/connect/types.go b/pkg/vatsim/connect/types.go index f05baba..b98f23d 100644 --- a/pkg/vatsim/connect/types.go +++ b/pkg/vatsim/connect/types.go @@ -16,54 +16,54 @@ type UserData struct { type Data struct { CID string `json:"cid"` - Personal Personal `json:"personal"` - Vatsim Vatsim `json:"vatsim"` + Personal Personal `json:"personal,omitempty"` + Vatsim Vatsim `json:"vatsim,omitempty"` } type Personal struct { - NameFirst string `json:"name_first"` - NameLast string `json:"name_last"` - NameFull string `json:"name_full"` - Email string `json:"email"` - Country Country `json:"country"` + NameFirst string `json:"name_first,omitempty"` + NameLast string `json:"name_last,omitempty"` + NameFull string `json:"name_full,omitempty"` + Email string `json:"email,omitempty"` + Country Country `json:"country,omitempty"` } type Country struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } type Vatsim struct { - Rating Rating `json:"rating"` - PilotRating PilotRating `json:"pilotrating"` - Region Region `json:"region"` - Division Division `json:"division"` - Subdivision Subdivision `json:"subdivision"` + Rating Rating `json:"rating,omitempty"` + PilotRating PilotRating `json:"pilotrating,omitempty"` + Region Region `json:"region,omitempty"` + Division Division `json:"division,omitempty"` + Subdivision Subdivision `json:"subdivision,omitempty"` } type Rating struct { - ID int `json:"id"` - Long string `json:"long"` - Short string `json:"short"` + ID int `json:"id,omitempty"` + Long string `json:"long,omitempty"` + Short string `json:"short,omitempty"` } type PilotRating struct { - ID int `json:"id"` - Long string `json:"long"` - Short string `json:"short"` + ID int `json:"id,omitempty"` + Long string `json:"long,omitempty"` + Short string `json:"short,omitempty"` } type Division struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } type Subdivision struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } type Region struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } From 3b9a8827f6b1c5d027eb8bb3d543ee086bda4723 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 20:52:02 +0200 Subject: [PATCH 08/15] Don't hard code the domain --- pkg/oauth2/authorize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/oauth2/authorize.go b/pkg/oauth2/authorize.go index abda8e0..fc78cb6 100644 --- a/pkg/oauth2/authorize.go +++ b/pkg/oauth2/authorize.go @@ -58,7 +58,7 @@ func Authorize(w http.ResponseWriter, r *http.Request) { Name: "token", Value: id, Path: "/oauth", - Domain: "localhost", + Domain: r.URL.Host, Expires: time.Now().UTC().Add(time.Minute*5), Secure: true, HttpOnly: true, From af5dfea4729ec1e97d5a86ca9e74d5d465eea373 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 23:19:10 +0200 Subject: [PATCH 09/15] Authorization works now --- Dockerfile | 6 ++-- pkg/models/oauth_auth_code.go | 1 + pkg/oauth2/authorize.go | 17 +++++----- pkg/oauth2/callback.go | 62 ++++++++++++++++++++++++++++++++++ pkg/oauth2/types.go | 2 ++ pkg/vatsim/connect/validate.go | 34 ++++++------------- pkg/web/types.go | 14 ++++++-- 7 files changed, 99 insertions(+), 37 deletions(-) create mode 100644 pkg/oauth2/callback.go diff --git a/Dockerfile b/Dockerfile index b33be4a..36b8627 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ FROM golang:1.17-alpine -WORKDIR ./ +WORKDIR . RUN go mod download -RUN go build -o /api /cmd/api/main.go +RUN go build /cmd/api/main.go EXPOSE 3000 -ENTRYPOINT [ "/api/main" ] \ No newline at end of file +ENTRYPOINT [ "main" ] \ No newline at end of file diff --git a/pkg/models/oauth_auth_code.go b/pkg/models/oauth_auth_code.go index b09c266..56adc36 100644 --- a/pkg/models/oauth_auth_code.go +++ b/pkg/models/oauth_auth_code.go @@ -8,6 +8,7 @@ type OauthAuthCode struct { Client OauthClient Scopes string `json:"-" gorm:"type:varchar(200);column:scopes"` UserAgent string `json:"-" gorm:"type:varchar(200);column:user_agent"` + State string `json:"-" gorm:"type:varchar(200);column:state;null"` CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` } diff --git a/pkg/oauth2/authorize.go b/pkg/oauth2/authorize.go index fc78cb6..bdaccbb 100644 --- a/pkg/oauth2/authorize.go +++ b/pkg/oauth2/authorize.go @@ -45,6 +45,7 @@ func Authorize(w http.ResponseWriter, r *http.Request) { Client: *client, Scopes: req.Scopes, UserAgent: r.UserAgent(), + State: req.State, } if err := database.DB.Create(&login).Error; err != nil { @@ -55,14 +56,14 @@ func Authorize(w http.ResponseWriter, r *http.Request) { } cookie := http.Cookie{ - Name: "token", - Value: id, - Path: "/oauth", - Domain: r.URL.Host, - Expires: time.Now().UTC().Add(time.Minute*5), - Secure: true, - HttpOnly: true, - SameSite: 1, + Name: cookieName, + Value: id, + Path: "/oauth", + Domain: r.URL.Host, + Expires: time.Now().UTC().Add(time.Minute * 5), + Secure: true, + HttpOnly: true, + SameSite: 1, } http.SetCookie(w, &cookie) diff --git a/pkg/oauth2/callback.go b/pkg/oauth2/callback.go new file mode 100644 index 0000000..b9cd2cc --- /dev/null +++ b/pkg/oauth2/callback.go @@ -0,0 +1,62 @@ +package oauth2 + +import ( + "api/internal/pkg/database" + "api/pkg/models" + "api/pkg/response" + "api/pkg/vatsim/connect" + "fmt" + "log" + "net/http" + "net/url" + "time" +) + +func Callback(w http.ResponseWriter, r *http.Request) { + _, err := connect.Validate(w, r) + + if err != nil { + log.Printf("Failed to fetch user details. Error: %s.\n", err.Error()) + res := response.New(w, r, "Failed to fetch user details.", http.StatusInternalServerError) + res.Process() + return + } + + cookie, err := r.Cookie(cookieName) + + if err != nil { + log.Printf("Failed to fetch the cookie. Error: %s.\n", err.Error()) + res := response.New(w, r, "Failed to retrieve the token.", http.StatusInternalServerError) + res.Process() + return + } + + defer func() { + cookie = &http.Cookie{ + Name: cookieName, + Value: "", + Path: "/", + Domain: r.URL.Host, + Expires: time.Now(), + } + + http.SetCookie(w, cookie) + }() + + authCode := models.OauthAuthCode{} + if err := database.DB.Debug().Preload("Client").Where("id = ? AND created_at > ?", cookie.Value, time.Now().UTC().Add(-time.Minute*5)).First(&authCode).Error; err != nil { + log.Printf("Failed to fetch the token. Error: %s.\n", err.Error()) + res := response.New(w, r, "Invalid token.", http.StatusInternalServerError) + res.Process() + return + } + + params := url.Values{} + params.Set("code", authCode.ID) + + if len(authCode.State) > 0 { + params.Set("state", authCode.State) + } + + http.Redirect(w, r, fmt.Sprintf("%s?%s", authCode.Client.Redirect, params.Encode()), http.StatusFound) +} diff --git a/pkg/oauth2/types.go b/pkg/oauth2/types.go index 9a66d9e..a170897 100644 --- a/pkg/oauth2/types.go +++ b/pkg/oauth2/types.go @@ -9,6 +9,8 @@ import ( "strings" ) +const cookieName = "token" + var scopes = []string{"full_name", "email", "vatsim_details", "country"} type authorizationRequest struct { diff --git a/pkg/vatsim/connect/validate.go b/pkg/vatsim/connect/validate.go index 7d924e1..80cd055 100644 --- a/pkg/vatsim/connect/validate.go +++ b/pkg/vatsim/connect/validate.go @@ -3,12 +3,10 @@ package connect import ( "api/internal/pkg/database" "api/pkg/models" - "api/pkg/response" "api/utils" "encoding/json" "errors" "fmt" - "github.com/golang-jwt/jwt/v4" "gorm.io/gorm" "io" "log" @@ -23,21 +21,16 @@ const ( grantType = "authorization_code" ) -func Validate(w http.ResponseWriter, r *http.Request) { +func Validate(w http.ResponseWriter, r *http.Request) (*UserData, error) { if err := r.URL.Query().Get("error"); len(err) > 0 { e := getError(err) - - res := response.New(w, r, e, http.StatusExpectationFailed) - res.Process() - return + return nil, errors.New(e) } code := r.URL.Query().Get("code") if len(code) < 0 { - res := response.New(w, r, "Code was not provided.", http.StatusBadRequest) - res.Process() - return + return nil, errors.New("code was not provided") } tokenChannel := make(chan Token) @@ -45,10 +38,7 @@ func Validate(w http.ResponseWriter, r *http.Request) { token := <-tokenChannel if token.err != nil { - log.Printf("Internal server error occurred while fetching the user details 44. Error: %s.", token.err.Error()) - res := response.New(w, r, "Internal server error occurred while fetching the user details 44.", http.StatusInternalServerError) - res.Process() - return + return nil, errors.New("failed to fetch the access token") } userChannel := make(chan UserData) @@ -56,23 +46,17 @@ func Validate(w http.ResponseWriter, r *http.Request) { user := <-userChannel if user.err != nil { - log.Printf("Internal server error occurred while fetching the user details 55. Error: %s.", user.err.Error()) - res := response.New(w, r, "Internal server error occurred while fetching the user details 55.", http.StatusInternalServerError) - res.Process() - return + return nil, errors.New("failed to fetch the user details") } saveChannel := make(chan error) go saveUser(user.Data, saveChannel) if err := <-saveChannel; err != nil { - log.Printf("Internal server error occurred while fetching the user details 66. Error: %s.", err.Error()) - res := response.New(w, r, "Internal server error occurred while fetching the user details 66.", http.StatusInternalServerError) - res.Process() - return + return nil, errors.New("failed to store the user") } - claims := jwt.MapClaims{ + /*claims := jwt.MapClaims{ "cid": user.Data.CID, } @@ -104,7 +88,9 @@ func Validate(w http.ResponseWriter, r *http.Request) { res := response.New(w, r, "Internal server error occurred while fetching the user details 102.", http.StatusInternalServerError) res.Process() return - } + }*/ + + return &user, nil } func getToken(code string, tokenChannel chan Token) { diff --git a/pkg/web/types.go b/pkg/web/types.go index 8a14b50..ae06975 100644 --- a/pkg/web/types.go +++ b/pkg/web/types.go @@ -68,7 +68,7 @@ func (server *Server) loadRoutes() { true, false, }, - { + /*{ "/auth/validate", []string{ "GET", @@ -77,7 +77,7 @@ func (server *Server) loadRoutes() { false, true, false, - }, + },*/ { "/api/user", []string{ @@ -258,6 +258,16 @@ func (server *Server) loadRoutes() { true, false, }, + { + "/oauth/callback", + []string{ + "GET", + }, + oauth2.Callback, + false, + true, + false, + }, } } From 7c0d488226814f263db4cfd963bc5233f29c7cd7 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 23:21:03 +0200 Subject: [PATCH 10/15] Remove debug --- pkg/oauth2/callback.go | 2 +- pkg/oauth2/token.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 pkg/oauth2/token.go diff --git a/pkg/oauth2/callback.go b/pkg/oauth2/callback.go index b9cd2cc..b37ca0a 100644 --- a/pkg/oauth2/callback.go +++ b/pkg/oauth2/callback.go @@ -44,7 +44,7 @@ func Callback(w http.ResponseWriter, r *http.Request) { }() authCode := models.OauthAuthCode{} - if err := database.DB.Debug().Preload("Client").Where("id = ? AND created_at > ?", cookie.Value, time.Now().UTC().Add(-time.Minute*5)).First(&authCode).Error; err != nil { + if err := database.DB.Preload("Client").Where("id = ? AND created_at > ?", cookie.Value, time.Now().UTC().Add(-time.Minute*5)).First(&authCode).Error; err != nil { log.Printf("Failed to fetch the token. Error: %s.\n", err.Error()) res := response.New(w, r, "Invalid token.", http.StatusInternalServerError) res.Process() diff --git a/pkg/oauth2/token.go b/pkg/oauth2/token.go new file mode 100644 index 0000000..ad618f8 --- /dev/null +++ b/pkg/oauth2/token.go @@ -0,0 +1,7 @@ +package oauth2 + +import "net/http" + +func Token(w http.ResponseWriter, r *http.Request) { + +} From d83fc366639a32c54aaae838dd9854a01b6da72e Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 12 Oct 2021 23:52:31 +0200 Subject: [PATCH 11/15] start working on access token endpoint --- pkg/oauth2/token.go | 15 ++++++++++++++- pkg/oauth2/types.go | 23 +++++++++++++++++++++++ pkg/web/types.go | 10 ++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/pkg/oauth2/token.go b/pkg/oauth2/token.go index ad618f8..5c197e0 100644 --- a/pkg/oauth2/token.go +++ b/pkg/oauth2/token.go @@ -1,7 +1,20 @@ package oauth2 -import "net/http" +import ( + "api/pkg/response" + "log" + "net/http" +) func Token(w http.ResponseWriter, r *http.Request) { + req, err := newAccessTokenRequest(r) + if err != nil { + log.Println("Error occurred while creating a new request. Error:", err.Error()) + res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) + res.Process() + return + } + + log.Println(req) } diff --git a/pkg/oauth2/types.go b/pkg/oauth2/types.go index a170897..fcb0f93 100644 --- a/pkg/oauth2/types.go +++ b/pkg/oauth2/types.go @@ -124,3 +124,26 @@ func (request authorizationRequest) isValidScope(scope string) bool { return false } + +type accessTokenRequest struct { + ContentType, GrantType, Code, RedirectURI, ClientID, ClientSecret string +} + +func newAccessTokenRequest(r *http.Request) (*accessTokenRequest, error) { + if r == nil { + return nil, errors.New("please provide a valid request") + } + + if err := r.ParseForm(); err != nil { + return nil, err + } + + return &accessTokenRequest{ + ContentType: r.Header.Get("Content-type"), + GrantType: r.PostForm.Get("grant_type"), + Code: r.PostForm.Get("code"), + RedirectURI: r.PostForm.Get("redirect_uri"), + ClientID: r.PostForm.Get("client_id"), + ClientSecret: r.PostForm.Get("client_secret"), + }, nil +} diff --git a/pkg/web/types.go b/pkg/web/types.go index ae06975..13fea26 100644 --- a/pkg/web/types.go +++ b/pkg/web/types.go @@ -268,6 +268,16 @@ func (server *Server) loadRoutes() { true, false, }, + { + "/oauth/token", + []string{ + "POST", + }, + oauth2.Token, + false, + true, + false, + }, } } From 21a4b248b233a5dafbf911e20f7e65d3fdec7427 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Thu, 14 Oct 2021 21:04:17 +0200 Subject: [PATCH 12/15] Start working on the access token endpoint --- pkg/oauth2/token.go | 20 ++++++- pkg/oauth2/types.go | 141 ++++++++++++++++++++++++++++++++++++++++++-- utils/empty.go | 5 ++ 3 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 utils/empty.go diff --git a/pkg/oauth2/token.go b/pkg/oauth2/token.go index 5c197e0..76a9a31 100644 --- a/pkg/oauth2/token.go +++ b/pkg/oauth2/token.go @@ -16,5 +16,23 @@ func Token(w http.ResponseWriter, r *http.Request) { return } - log.Println(req) + _, aErr := req.Validate() + + if aErr != nil { + log.Println("Validation failed. Error:", aErr.internalError.Error()) + data, err := aErr.Json() + + if err != nil { + log.Println("Error occurred while marshalling the json response. Error:", err.Error()) + res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) + res.Process() + return + } + + w.WriteHeader(aErr.Code) + if _, err := w.Write(data); err != nil { + log.Println("failed to write") + return + } + } } diff --git a/pkg/oauth2/types.go b/pkg/oauth2/types.go index fcb0f93..5573974 100644 --- a/pkg/oauth2/types.go +++ b/pkg/oauth2/types.go @@ -3,13 +3,18 @@ package oauth2 import ( "api/internal/pkg/database" "api/pkg/models" + "api/utils" + "encoding/json" "errors" "net/http" "net/url" "strings" ) -const cookieName = "token" +const ( + cookieName = "token" + contentType = "application/x-www-form-urlencoded" +) var scopes = []string{"full_name", "email", "vatsim_details", "country"} @@ -17,6 +22,17 @@ type authorizationRequest struct { ResponseType, ClientID, RedirectURI, State, Scopes string } +type accessTokenError struct { + Err string `json:"error"` + Description string `json:"error_description"` + Code int `json:"-"` + internalError error +} + +func (err *accessTokenError) Json() ([]byte, error) { + return json.Marshal(err) +} + type requestError struct { Response url.Values } @@ -83,10 +99,10 @@ func (request *authorizationRequest) Validate() (*models.OauthClient, error) { return &client, nil } -func (request *authorizationRequest) formatURL(err string) url.Values { +func (request authorizationRequest) formatURL(err string) url.Values { val := url.Values{} val.Set("error", err) - val.Set("error_description", request.getError(err)) + val.Set("error_description", getError(err)) if len(request.State) > 0 { val.Set("state", request.State) @@ -95,7 +111,7 @@ func (request *authorizationRequest) formatURL(err string) url.Values { return val } -func (request authorizationRequest) getError(err string) string { +func getError(err string) string { authErrors := map[string]string{ "invalid_request": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", "unauthorized_client": "The client is not authorized to request an authorization code using this method.", @@ -104,6 +120,9 @@ func (request authorizationRequest) getError(err string) string { "invalid_scope": "The requested scope is invalid, unknown, or malformed.", "server_error": "The authorization server encountered an unexpected condition that prevented it from fulfilling the request.", "temporarily_unavailable": "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.", + "invalid_grant": "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.", + "invalid_client": "Client authentication failed.", + "unsupported_grant_type": "The authorization grant type is not supported by the authorization server.", } e, ok := authErrors[err] @@ -126,7 +145,7 @@ func (request authorizationRequest) isValidScope(scope string) bool { } type accessTokenRequest struct { - ContentType, GrantType, Code, RedirectURI, ClientID, ClientSecret string + ContentType, GrantType, Code, RedirectURI, ClientID, ClientSecret, State string } func newAccessTokenRequest(r *http.Request) (*accessTokenRequest, error) { @@ -145,5 +164,117 @@ func newAccessTokenRequest(r *http.Request) (*accessTokenRequest, error) { RedirectURI: r.PostForm.Get("redirect_uri"), ClientID: r.PostForm.Get("client_id"), ClientSecret: r.PostForm.Get("client_secret"), + State: r.PostForm.Get("state"), }, nil } + +func (r accessTokenRequest) Validate() (*models.OauthClient, *accessTokenError) { + if !r.hasRequiredAttributes() { + return nil, &accessTokenError{ + Err: "invalid_request", + Code: http.StatusBadRequest, + Description: getError("invalid_request"), + internalError: errors.New("required parameters weren't provided"), + } + } + + if r.ContentType != contentType { + return nil, &accessTokenError{ + Err: "invalid_request", + Code: http.StatusBadRequest, + Description: "Invalid content type provided", + internalError: errors.New("invalid content type provided"), + } + } + + if r.GrantType != "authorization_code" { + return nil, &accessTokenError{ + Err: "invalid_grant", + Code: http.StatusBadRequest, + Description: getError("invalid_grant"), + internalError: errors.New("invalid grant type provided"), + } + } + + code := models.OauthAuthCode{} + if err := database.DB.Where("id = ?", r.Code).First(&code).Error; err != nil { + return nil, &accessTokenError{ + Err: "invalid_client", + Code: http.StatusUnauthorized, + Description: getError("invalid_client"), + internalError: errors.New("auth code not found"), + } + } + + client := models.OauthClient{} + if err := database.DB.Where("id = ? AND secret = ?", r.ClientID, r.ClientSecret).First(&client).Error; err != nil { + return nil, &accessTokenError{ + Err: "invalid_client", + Code: http.StatusUnauthorized, + Description: getError("invalid_client"), + internalError: errors.New("client not found"), + } + } + + if client.Revoked { + return nil, &accessTokenError{ + Err: "invalid_client", + Code: http.StatusUnauthorized, + Description: getError("invalid_client"), + internalError: errors.New("client is revoked"), + } + } + + if client.Secret != r.ClientSecret { + return nil, &accessTokenError{ + Err: "invalid_client", + Code: http.StatusUnauthorized, + Description: getError("invalid_client"), + internalError: errors.New("client secret does not match"), + } + } + + if client.ID != code.ClientID { + return nil, &accessTokenError{ + Err: "invalid_client", + Code: http.StatusUnauthorized, + Description: getError("invalid_client"), + internalError: errors.New("auth code client and provided client don't match not found"), + } + } + + if !client.IsValidRedirectURI(r.RedirectURI) { + return nil, &accessTokenError{ + Err: "invalid_client", + Code: http.StatusUnauthorized, + Description: getError("invalid_client"), + internalError: errors.New("invalid redirect URI"), + } + } + + return &client, nil +} + +func (r accessTokenRequest) hasRequiredAttributes() bool { + if utils.IsEmptyString(r.GrantType) { + return false + } + + if utils.IsEmptyString(r.Code) { + return false + } + + if utils.IsEmptyString(r.RedirectURI) { + return false + } + + if utils.IsEmptyString(r.ClientSecret) { + return false + } + + if utils.IsEmptyString(r.ClientID) { + return false + } + + return true +} diff --git a/utils/empty.go b/utils/empty.go new file mode 100644 index 0000000..1d91a7c --- /dev/null +++ b/utils/empty.go @@ -0,0 +1,5 @@ +package utils + +func IsEmptyString(data string) bool { + return len(data) < 1 +} From 356c67bcb2ba9e120307b63407db9b9316da6210 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Thu, 14 Oct 2021 21:07:11 +0200 Subject: [PATCH 13/15] Add headers --- pkg/oauth2/token.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/oauth2/token.go b/pkg/oauth2/token.go index 76a9a31..dd1e3c7 100644 --- a/pkg/oauth2/token.go +++ b/pkg/oauth2/token.go @@ -7,6 +7,9 @@ import ( ) func Token(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") req, err := newAccessTokenRequest(r) if err != nil { From a9b2aaeb6ade165235fb7fe2a26a72895aacdff3 Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Thu, 14 Oct 2021 22:16:41 +0200 Subject: [PATCH 14/15] Add JWT --- pkg/oauth2/token.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pkg/oauth2/token.go b/pkg/oauth2/token.go index dd1e3c7..e4105ba 100644 --- a/pkg/oauth2/token.go +++ b/pkg/oauth2/token.go @@ -2,6 +2,9 @@ package oauth2 import ( "api/pkg/response" + "api/utils" + "encoding/json" + "github.com/golang-jwt/jwt/v4" "log" "net/http" ) @@ -38,4 +41,33 @@ func Token(w http.ResponseWriter, r *http.Request) { return } } + + claims := jwt.MapClaims{ + "": "", + } + token, err := utils.GenerateNewJWT(claims) + + if err != nil { + log.Println("Error occurred while generating the JWT token. Error:", err.Error()) + res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) + res.Process() + return + } + + data := map[string]interface{}{ + "access_token": token, + "expires_in": 60 * 60 * 24 * 7, + } + + bytes, err := json.Marshal(data) + + if err != nil { + log.Println("Error occurred while marshalling the json response. Error:", err.Error()) + res := response.New(w, r, "Internal server error occurred.", http.StatusInternalServerError) + res.Process() + return + } + + w.WriteHeader(http.StatusOK) + w.Write(bytes) } From e7a0d6366a8e6514f5393d8cb738ecd10b0e262f Mon Sep 17 00:00:00 2001 From: Matan Budimir Date: Tue, 26 Oct 2021 22:23:11 +0200 Subject: [PATCH 15/15] Fixes --- pkg/web/types.go | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/web/types.go b/pkg/web/types.go index c06b6e7..cfcced7 100644 --- a/pkg/web/types.go +++ b/pkg/web/types.go @@ -84,7 +84,13 @@ func (server *Server) loadRoutes() { []string{ "GET", }, - connect.Validate, + oauth2.User, + Permission{ + AuthNeeded: false, + GuestOnly: false, + AllowCors: false, + SubdivisionToken: false, + }, }, { "/api/user", @@ -92,6 +98,7 @@ func (server *Server) loadRoutes() { "GET", }, oauth2.User, + Permission{ true, false, true, @@ -390,9 +397,12 @@ func (server *Server) loadRoutes() { "GET", }, oauth2.Authorize, - false, - true, - false, + Permission{ + false, + true, + false, + false, + }, }, { "/oauth/callback", @@ -400,9 +410,12 @@ func (server *Server) loadRoutes() { "GET", }, oauth2.Callback, - false, - true, - false, + Permission{ + false, + true, + false, + false, + }, }, { "/oauth/token", @@ -410,9 +423,12 @@ func (server *Server) loadRoutes() { "POST", }, oauth2.Token, - false, - true, - false, + Permission{ + false, + true, + false, + false, + }, }, } }