Commit 6f4b8a7111172ca9c9aa5db9898cfcfaf0b350df
1 parent
4a51e54d7e
Exists in
master
and in
1 other branch
token response changed
Showing
3 changed files
with
14 additions
and
17 deletions
Show diff stats
README.md
1 | TODO: | 1 | TODO: |
2 | * http utility: | 2 | * http utility: |
3 | 1. add parameters to the ProcessHeaders to enable/disable token/role-access-rights checks | 3 | 1. add parameters to the ProcessHeaders to enable/disable token/role-access-rights checks |
4 | 2. check for Content-Type header, if clients expects something other than JSON respond with appropriate HTTP code | ||
4 | 5 |
auth_utility.go
1 | package restutility | 1 | package restutility |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "errors" | 4 | "errors" |
5 | "time" | 5 | "time" |
6 | "crypto/sha256" | 6 | "crypto/sha256" |
7 | "crypto/rand" | 7 | "crypto/rand" |
8 | "encoding/hex" | 8 | "encoding/hex" |
9 | "strings" | 9 | "strings" |
10 | "github.com/dgrijalva/jwt-go" | 10 | "github.com/dgrijalva/jwt-go" |
11 | ) | 11 | ) |
12 | 12 | ||
13 | const OneDay = time.Hour*24 | 13 | const OneDay = time.Hour*24 |
14 | const OneWeek = OneDay*7 | 14 | const OneWeek = OneDay*7 |
15 | const saltSize = 32 | 15 | const saltSize = 32 |
16 | const appName = "korisnicki-centar" | 16 | const appName = "korisnicki-centar" |
17 | const secret = "korisnicki-centar-api" | 17 | const secret = "korisnicki-centar-api" |
18 | 18 | ||
19 | type Token struct { | ||
20 | TokenString string `json:"token"` | ||
21 | } | ||
22 | |||
23 | type TokenClaims struct { | 19 | type TokenClaims struct { |
24 | Username string `json:"username"` | 20 | Username string `json:"username"` |
25 | Role string `json:"role"` | 21 | Role string `json:"role"` |
26 | jwt.StandardClaims | 22 | jwt.StandardClaims |
27 | } | 23 | } |
28 | 24 | ||
29 | type CredentialsStruct struct { | 25 | type CredentialsStruct struct { |
30 | Username string `json:"username"` | 26 | Username string `json:"username"` |
31 | Password string `json:"password"` | 27 | Password string `json:"password"` |
32 | } | 28 | } |
33 | 29 | ||
34 | func GenerateSalt() (string, error) { | 30 | func GenerateSalt() (string, error) { |
35 | salt := "" | 31 | salt := "" |
36 | 32 | ||
37 | rawsalt := make([]byte, saltSize) | 33 | rawsalt := make([]byte, saltSize) |
38 | _, err := rand.Read(rawsalt) | 34 | _, err := rand.Read(rawsalt) |
39 | if err != nil { | 35 | if err != nil { |
40 | return "", err | 36 | return "", err |
41 | } | 37 | } |
42 | salt = hex.EncodeToString(rawsalt) | 38 | salt = hex.EncodeToString(rawsalt) |
43 | return salt, nil | 39 | return salt, nil |
44 | } | 40 | } |
45 | 41 | ||
46 | func HashMessage(message string, presalt string) (string, string, error) { | 42 | func HashMessage(message string, presalt string) (string, string, error) { |
47 | hash, salt := "", "" | 43 | hash, salt := "", "" |
48 | var err error | 44 | var err error |
49 | 45 | ||
50 | // chech if message is presalted | 46 | // chech if message is presalted |
51 | if presalt == "" { | 47 | if presalt == "" { |
52 | salt, err = GenerateSalt() | 48 | salt, err = GenerateSalt() |
53 | if err != nil { | 49 | if err != nil { |
54 | return "", "", err | 50 | return "", "", err |
55 | } | 51 | } |
56 | } else { | 52 | } else { |
57 | salt = presalt | 53 | salt = presalt |
58 | } | 54 | } |
59 | 55 | ||
60 | // convert strings to raw byte slices | 56 | // convert strings to raw byte slices |
61 | rawmessage := []byte(message) | 57 | rawmessage := []byte(message) |
62 | rawsalt, err := hex.DecodeString(salt) | 58 | rawsalt, err := hex.DecodeString(salt) |
63 | if err != nil { | 59 | if err != nil { |
64 | return "", "", err | 60 | return "", "", err |
65 | } | 61 | } |
66 | rawdata := make([]byte, len(rawmessage) + len(rawsalt)) | 62 | rawdata := make([]byte, len(rawmessage) + len(rawsalt)) |
67 | rawdata = append(rawdata, rawmessage...) | 63 | rawdata = append(rawdata, rawmessage...) |
68 | rawdata = append(rawdata, rawsalt...) | 64 | rawdata = append(rawdata, rawsalt...) |
69 | 65 | ||
70 | // hash message + salt | 66 | // hash message + salt |
71 | hasher := sha256.New() | 67 | hasher := sha256.New() |
72 | hasher.Write(rawdata) | 68 | hasher.Write(rawdata) |
73 | rawhash := hasher.Sum(nil) | 69 | rawhash := hasher.Sum(nil) |
74 | hash = hex.EncodeToString(rawhash) | 70 | hash = hex.EncodeToString(rawhash) |
75 | return hash, salt, nil | 71 | return hash, salt, nil |
76 | } | 72 | } |
77 | 73 | ||
78 | func IssueAPIToken(username, role string) (Token, error) { | 74 | func IssueAPIToken(username, role string) (string, error) { |
79 | var apiToken Token | 75 | var apiToken string |
80 | var err error | 76 | var err error |
81 | 77 | ||
82 | if err != nil { | 78 | if err != nil { |
83 | return Token{}, err | 79 | return "", err |
84 | } | 80 | } |
85 | 81 | ||
86 | claims := TokenClaims{ | 82 | claims := TokenClaims{ |
87 | username, | 83 | username, |
88 | role, | 84 | role, |
89 | jwt.StandardClaims{ | 85 | jwt.StandardClaims{ |
90 | ExpiresAt: (time.Now().Add(OneWeek)).Unix(), | 86 | ExpiresAt: (time.Now().Add(OneWeek)).Unix(), |
91 | Issuer: appName, | 87 | Issuer: appName, |
92 | }, | 88 | }, |
93 | } | 89 | } |
94 | 90 | ||
95 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | 91 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) |
96 | apiToken.TokenString, err = jwtToken.SignedString([]byte(secret)) | 92 | apiToken, err = jwtToken.SignedString([]byte(secret)) |
97 | if err != nil { | 93 | if err != nil { |
98 | return Token{}, err | 94 | return "", err |
99 | } | 95 | } |
100 | return apiToken, nil | 96 | return apiToken, nil |
101 | } | 97 | } |
102 | 98 | ||
103 | func RefreshAPIToken(tokenString string) (Token, error) { | 99 | func RefreshAPIToken(tokenString string) (string, error) { |
104 | var newToken Token | 100 | var newToken string |
105 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") | 101 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") |
106 | token, err := parseTokenFunc(tokenString) | 102 | token, err := parseTokenFunc(tokenString) |
107 | if err != nil { | 103 | if err != nil { |
108 | return Token{}, err | 104 | return "", err |
109 | } | 105 | } |
110 | 106 | ||
111 | // type assertion | 107 | // type assertion |
112 | claims, ok := token.Claims.(*TokenClaims) | 108 | claims, ok := token.Claims.(*TokenClaims) |
113 | if !ok || !token.Valid { | 109 | if !ok || !token.Valid { |
114 | return Token{}, errors.New("token is not valid") | 110 | return "", errors.New("token is not valid") |
115 | } | 111 | } |
116 | 112 | ||
117 | claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix() | 113 | claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix() |
118 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | 114 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) |
119 | 115 | ||
120 | newToken.TokenString, err = jwtToken.SignedString([]byte(secret)) | 116 | newToken, err = jwtToken.SignedString([]byte(secret)) |
121 | if err != nil { | 117 | if err != nil { |
122 | return Token{}, err | 118 | return "", err |
123 | } | 119 | } |
124 | 120 | ||
125 | return newToken, nil | 121 | return newToken, nil |
126 | } | 122 | } |
127 | 123 | ||
128 | func ParseAPIToken(tokenString string) (*TokenClaims, error) { | 124 | func ParseAPIToken(tokenString string) (*TokenClaims, error) { |
129 | if ok := strings.HasPrefix(tokenString, "Bearer"); ok { | 125 | if ok := strings.HasPrefix(tokenString, "Bearer"); ok { |
130 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") | 126 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") |
131 | } else { | 127 | } else { |
132 | return &TokenClaims{}, errors.New("Authorization header is incomplete") | 128 | return &TokenClaims{}, errors.New("Authorization header is incomplete") |
133 | } | 129 | } |
134 | 130 | ||
135 | token, err := parseTokenFunc(tokenString) | 131 | token, err := parseTokenFunc(tokenString) |
136 | if err != nil { | 132 | if err != nil { |
137 | return &TokenClaims{}, err | 133 | return &TokenClaims{}, err |
138 | } | 134 | } |
139 | 135 | ||
140 | // type assertion | 136 | // type assertion |
141 | claims, ok := token.Claims.(*TokenClaims) | 137 | claims, ok := token.Claims.(*TokenClaims) |
142 | if !ok || !token.Valid { | 138 | if !ok || !token.Valid { |
143 | return &TokenClaims{}, errors.New("token is not valid") | 139 | return &TokenClaims{}, errors.New("token is not valid") |
144 | } | 140 | } |
145 | return claims, nil | 141 | return claims, nil |
146 | } | 142 | } |
147 | 143 | ||
148 | func parseTokenFunc(tokenString string) (*jwt.Token, error) { | 144 | func parseTokenFunc(tokenString string) (*jwt.Token, error) { |
149 | token, err := jwt.ParseWithClaims(tokenString, | 145 | token, err := jwt.ParseWithClaims(tokenString, |
150 | &TokenClaims{}, | 146 | &TokenClaims{}, |
151 | func(token *jwt.Token) (interface{}, error) { | 147 | func(token *jwt.Token) (interface{}, error) { |
152 | return []byte(secret), nil | 148 | return []byte(secret), nil |
153 | }, | 149 | }, |
154 | ) | 150 | ) |
155 | return token, err | 151 | return token, err |
156 | } | 152 | } |
157 | 153 |
http_utility.go
1 | package restutility | 1 | package restutility |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "net/http" | 4 | "net/http" |
5 | "encoding/json" | 5 | "encoding/json" |
6 | ) | 6 | ) |
7 | 7 | ||
8 | var _apiVersion = "" | 8 | var _apiVersion = "" |
9 | var _authEndPoint = "" | 9 | var _authEndPoint = "" |
10 | 10 | ||
11 | func SetApiVersion(ver string) string { | 11 | func SetApiVersion(ver string) string { |
12 | _apiVersion = ver | 12 | _apiVersion = ver |
13 | return _apiVersion | 13 | return _apiVersion |
14 | } | 14 | } |
15 | 15 | ||
16 | func SetAuthEndpoint(ep string) { | 16 | func SetAuthEndpoint(ep string) { |
17 | _authEndPoint = ep | 17 | _authEndPoint = ep |
18 | } | 18 | } |
19 | 19 | ||
20 | //// | 20 | //// |
21 | //// ERROR UTILITY | 21 | //// ERROR UTILITY |
22 | //// | 22 | //// |
23 | 23 | ||
24 | const templateHttpErr500_EN = "An internal server error has occurred." | 24 | const templateHttpErr500_EN = "An internal server error has occurred." |
25 | const templateHttpErr500_RS = "Došlo je do greške na serveru." | 25 | const templateHttpErr500_RS = "Došlo je do greške na serveru." |
26 | const templateHttpErr400_EN = "Bad request: invalid request body." | 26 | const templateHttpErr400_EN = "Bad request: invalid request body." |
27 | const templateHttpErr400_RS = "Neispravan zahtev." | 27 | const templateHttpErr400_RS = "Neispravan zahtev." |
28 | 28 | ||
29 | type HttpError struct { | 29 | type HttpError struct { |
30 | Error []HttpErrorDesc `json:"error"` | 30 | Error []HttpErrorDesc `json:"error"` |
31 | Request string `json:"request"` | 31 | Request string `json:"request"` |
32 | } | 32 | } |
33 | 33 | ||
34 | type HttpErrorDesc struct { | 34 | type HttpErrorDesc struct { |
35 | Lang string `json:"lang"` | 35 | Lang string `json:"lang"` |
36 | Desc string `json:"description"` | 36 | Desc string `json:"description"` |
37 | } | 37 | } |
38 | 38 | ||
39 | func RespondWithHttpError(w http.ResponseWriter, | 39 | func RespondWithHttpError(w http.ResponseWriter, |
40 | req *http.Request, | 40 | req *http.Request, |
41 | code int, | 41 | code int, |
42 | httpErr []HttpErrorDesc) { | 42 | httpErr []HttpErrorDesc) { |
43 | 43 | ||
44 | err := HttpError{ | 44 | err := HttpError{ |
45 | Error: httpErr, | 45 | Error: httpErr, |
46 | Request: req.Method + " " + req.URL.Path, | 46 | Request: req.Method + " " + req.URL.Path, |
47 | } | 47 | } |
48 | w.WriteHeader(code) | 48 | w.WriteHeader(code) |
49 | json.NewEncoder(w).Encode(err) | 49 | json.NewEncoder(w).Encode(err) |
50 | } | 50 | } |
51 | 51 | ||
52 | func RespondWithHttpError400(w http.ResponseWriter, req *http.Request) { | 52 | func RespondWithHttpError400(w http.ResponseWriter, req *http.Request) { |
53 | RespondWithHttpError(w, req, http.StatusBadRequest, []HttpErrorDesc{ | 53 | RespondWithHttpError(w, req, http.StatusBadRequest, []HttpErrorDesc{ |
54 | {Lang: "en", Desc: templateHttpErr400_EN}, | 54 | {Lang: "en", Desc: templateHttpErr400_EN}, |
55 | {Lang: "rs", Desc: templateHttpErr400_RS}, | 55 | {Lang: "rs", Desc: templateHttpErr400_RS}, |
56 | }) | 56 | }) |
57 | } | 57 | } |
58 | 58 | ||
59 | func RespondWithHttpError500(w http.ResponseWriter, req *http.Request) { | 59 | func RespondWithHttpError500(w http.ResponseWriter, req *http.Request) { |
60 | RespondWithHttpError(w, req, http.StatusInternalServerError, []HttpErrorDesc{ | 60 | RespondWithHttpError(w, req, http.StatusInternalServerError, []HttpErrorDesc{ |
61 | {Lang: "en", Desc: templateHttpErr500_EN}, | 61 | {Lang: "en", Desc: templateHttpErr500_EN}, |
62 | {Lang: "rs", Desc: templateHttpErr500_RS}, | 62 | {Lang: "rs", Desc: templateHttpErr500_RS}, |
63 | }) | 63 | }) |
64 | } | 64 | } |
65 | 65 | ||
66 | //// | 66 | //// |
67 | //// HANDLER FUNC WRAPPER | 67 | //// HANDLER FUNC WRAPPER |
68 | //// | 68 | //// |
69 | 69 | ||
70 | //TODO: Add parameters to enable/disable token and roles authorization checks | 70 | //TODO: Add parameters to enable/disable token and roles authorization checks |
71 | // Sets common headers and checks for token validity. | 71 | // Sets common headers and checks for token validity. |
72 | func ProcessHeaders(fn http.HandlerFunc, shouldAuth bool) http.HandlerFunc { | 72 | func ProcessHeaders(fn http.HandlerFunc, authEnabled bool) http.HandlerFunc { |
73 | return func(w http.ResponseWriter, req *http.Request) { | 73 | return func(w http.ResponseWriter, req *http.Request) { |
74 | // @TODO: check Content-type header (must be application/json) | 74 | // @TODO: check Content-type header (must be application/json) |
75 | // ctype := w.Header.Get("Content-Type") | 75 | // ctype := w.Header.Get("Content-Type") |
76 | // if req.Method != "GET" && ctype != "application/json" { | 76 | // if req.Method != "GET" && ctype != "application/json" { |
77 | // replyWithHttpError(w, req, http.StatusBadRequest, | 77 | // replyWithHttpError(w, req, http.StatusBadRequest, |
78 | // "Not a supported content type: " + ctype) | 78 | // "Not a supported content type: " + ctype) |
79 | // } | 79 | // } |
80 | 80 | ||
81 | w.Header().Set("Access-Control-Allow-Origin", "*") | 81 | w.Header().Set("Access-Control-Allow-Origin", "*") |
82 | w.Header().Set("Access-Control-Allow-Methods", | 82 | w.Header().Set("Access-Control-Allow-Methods", |
83 | `POST, | 83 | `POST, |
84 | GET, | 84 | GET, |
85 | PUT, | 85 | PUT, |
86 | DELETE, | 86 | DELETE, |
87 | OPTIONS`) | 87 | OPTIONS`) |
88 | w.Header().Set("Access-Control-Allow-Headers", | 88 | w.Header().Set("Access-Control-Allow-Headers", |
89 | `Accept, | 89 | `Accept, |
90 | Content-Type, | 90 | Content-Type, |
91 | Content-Length, | 91 | Content-Length, |
92 | Accept-Encoding, | 92 | Accept-Encoding, |
93 | X-CSRF-Token, | 93 | X-CSRF-Token, |
94 | Authorization`) | 94 | Authorization`) |
95 | w.Header().Set("Content-Type", "application/json; charset=utf-8") | 95 | w.Header().Set("Content-Type", "application/json; charset=utf-8") |
96 | 96 | ||
97 | if req.Method == "OPTIONS" { | 97 | if req.Method == "OPTIONS" { |
98 | return | 98 | return |
99 | } | 99 | } |
100 | 100 | ||
101 | if shouldAuth { | 101 | if authEnabled { |
102 | if req.URL.Path != _apiVersion + _authEndPoint { | 102 | if req.URL.Path != _apiVersion + _authEndPoint { |
103 | token := req.Header.Get("Authorization") | 103 | token := req.Header.Get("Authorization") |
104 | if _, err := ParseAPIToken(token); err != nil { | 104 | if _, err := ParseAPIToken(token); err != nil { |
105 | RespondWithHttpError(w, req, http.StatusUnauthorized, | 105 | RespondWithHttpError(w, req, http.StatusUnauthorized, |
106 | []HttpErrorDesc{ | 106 | []HttpErrorDesc{ |
107 | {Lang: "en", Desc: "Unauthorized request."}, | 107 | {Lang: "en", Desc: "Unauthorized request."}, |
108 | {Lang: "rs", Desc: "Neautorizovani zahtev."}, | 108 | {Lang: "rs", Desc: "Neautorizovani zahtev."}, |
109 | }) | 109 | }) |
110 | return | 110 | return |
111 | } | 111 | } |
112 | } | 112 | } |
113 | } | 113 | } |
114 | 114 | ||
115 | err := req.ParseForm() | 115 | err := req.ParseForm() |
116 | if err != nil { | 116 | if err != nil { |
117 | RespondWithHttpError(w, req, http.StatusBadRequest, | 117 | RespondWithHttpError(w, req, http.StatusBadRequest, |
118 | []HttpErrorDesc{ | 118 | []HttpErrorDesc{ |
119 | {Lang: "en", Desc: templateHttpErr400_EN}, | 119 | {Lang: "en", Desc: templateHttpErr400_EN}, |
120 | {Lang: "rs", Desc: templateHttpErr400_RS}, | 120 | {Lang: "rs", Desc: templateHttpErr400_RS}, |
121 | }) | 121 | }) |
122 | return | 122 | return |
123 | } | 123 | } |
124 | 124 | ||
125 | // execute HandlerFunc | 125 | // execute HandlerFunc |
126 | fn(w, req) | 126 | fn(w, req) |
127 | } | 127 | } |
128 | } | 128 | } |
129 | 129 | ||
130 | //// | 130 | //// |
131 | //// NOT FOUND HANDLER | 131 | //// NOT FOUND HANDLER |
132 | //// | 132 | //// |
133 | 133 | ||
134 | func NotFoundHandler(w http.ResponseWriter, req *http.Request) { | 134 | func NotFoundHandler(w http.ResponseWriter, req *http.Request) { |
135 | RespondWithHttpError(w, req, http.StatusNotFound, []HttpErrorDesc{ | 135 | RespondWithHttpError(w, req, http.StatusNotFound, []HttpErrorDesc{ |
136 | {Lang: "en", Desc: "Not found."}, | 136 | {Lang: "en", Desc: "Not found."}, |
137 | {Lang: "rs", Desc: "Traženi resurs ne postoji."}, | 137 | {Lang: "rs", Desc: "Traženi resurs ne postoji."}, |
138 | }) | 138 | }) |
139 | } | 139 | } |
140 | 140 |