├── .codeclimate.yml ├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── README.md ├── authhandler.go ├── circle.yml ├── jwtutil.go ├── main.go ├── main_test.go └── static └── authenticate.html /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | gofmt: 3 | enabled: true 4 | govet: 5 | enabled: true 6 | golint: 7 | enabled: true 8 | ratings: 9 | paths: 10 | - "**.go" 11 | exclude_paths: 12 | - "**_test.go" 13 | - vendor/** 14 | - static/** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | .project 3 | .settings 4 | .vscode 5 | 6 | # Dependencies 7 | vendor/ 8 | vendors/ 9 | 10 | # Coverage Reports 11 | reports/ 12 | coverage.out 13 | coverage.html 14 | coverage.txt 15 | junitFormatReport.xml 16 | 17 | # Executable 18 | go-mux-jwt 19 | 20 | # mac crap 21 | .DS_Store 22 | 23 | # livereload helper 24 | gin-bin 25 | 26 | # Logs 27 | logs 28 | 29 | *.log 30 | 31 | # Runtime data 32 | pids 33 | *.pid 34 | *.seed -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/davecgh/go-spew" 6 | packages = ["spew"] 7 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 8 | version = "v1.1.0" 9 | 10 | [[projects]] 11 | name = "github.com/dgrijalva/jwt-go" 12 | packages = ["."] 13 | revision = "d2709f9f1f31ebcda9651b03077758c1f3a0018c" 14 | version = "v3.0.0" 15 | 16 | [[projects]] 17 | name = "github.com/gorilla/context" 18 | packages = ["."] 19 | revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" 20 | version = "v1.1" 21 | 22 | [[projects]] 23 | name = "github.com/gorilla/handlers" 24 | packages = ["."] 25 | revision = "a4043c62cc2329bacda331d33fc908ab11ef0ec3" 26 | version = "v1.2.1" 27 | 28 | [[projects]] 29 | name = "github.com/gorilla/mux" 30 | packages = ["."] 31 | revision = "bcd8bc72b08df0f70df986b97f95590779502d31" 32 | version = "v1.4.0" 33 | 34 | [[projects]] 35 | name = "github.com/pmezard/go-difflib" 36 | packages = ["difflib"] 37 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 38 | version = "v1.0.0" 39 | 40 | [[projects]] 41 | branch = "master" 42 | name = "github.com/stretchr/testify" 43 | packages = ["assert"] 44 | revision = "f6abca593680b2315d2075e0f5e2a9751e3f431a" 45 | 46 | [solve-meta] 47 | analyzer-name = "dep" 48 | analyzer-version = 1 49 | inputs-digest = "a64c7794c932c758830b3318151362d04b9bdfeaa3925d8a1179257fca7d0e2d" 50 | solver-name = "gps-cdcl" 51 | solver-version = 1 52 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | ## Gopkg.toml example (these lines may be deleted) 3 | 4 | ## "metadata" defines metadata about the project that could be used by other independent 5 | ## systems. The metadata defined here will be ignored by dep. 6 | # [metadata] 7 | # key1 = "value that convey data to other systems" 8 | # system1-data = "value that is used by a system" 9 | # system2-data = "value that is used by another system" 10 | 11 | ## "required" lists a set of packages (not projects) that must be included in 12 | ## Gopkg.lock. This list is merged with the set of packages imported by the current 13 | ## project. Use it when your project needs a package it doesn't explicitly import - 14 | ## including "main" packages. 15 | # required = ["github.com/user/thing/cmd/thing"] 16 | 17 | ## "ignored" lists a set of packages (not projects) that are ignored when 18 | ## dep statically analyzes source code. Ignored packages can be in this project, 19 | ## or in a dependency. 20 | # ignored = ["github.com/user/project/badpkg"] 21 | 22 | ## Constraints are rules for how directly imported projects 23 | ## may be incorporated into the depgraph. They are respected by 24 | ## dep whether coming from the Gopkg.toml of the current project or a dependency. 25 | # [[constraint]] 26 | ## Required: the root import path of the project being constrained. 27 | # name = "github.com/user/project" 28 | # 29 | ## Recommended: the version constraint to enforce for the project. 30 | ## Only one of "branch", "version" or "revision" can be specified. 31 | # version = "1.0.0" 32 | # branch = "master" 33 | # revision = "abc123" 34 | # 35 | ## Optional: an alternate location (URL or import path) for the project's source. 36 | # source = "https://github.com/myfork/package.git" 37 | # 38 | ## "metadata" defines metadata about the dependency or override that could be used 39 | ## by other independent systems. The metadata defined here will be ignored by dep. 40 | # [metadata] 41 | # key1 = "value that convey data to other systems" 42 | # system1-data = "value that is used by a system" 43 | # system2-data = "value that is used by another system" 44 | 45 | ## Overrides have the same structure as [[constraint]], but supersede all 46 | ## [[constraint]] declarations from all projects. Only [[override]] from 47 | ## the current project's are applied. 48 | ## 49 | ## Overrides are a sledgehammer. Use them only as a last resort. 50 | # [[override]] 51 | ## Required: the root import path of the project being constrained. 52 | # name = "github.com/user/project" 53 | # 54 | ## Optional: specifying a version constraint override will cause all other 55 | ## constraints on this project to be ignored; only the overridden constraint 56 | ## need be satisfied. 57 | ## Again, only one of "branch", "version" or "revision" can be specified. 58 | # version = "1.0.0" 59 | # branch = "master" 60 | # revision = "abc123" 61 | # 62 | ## Optional: specifying an alternate source location as an override will 63 | ## enforce that the alternate location is used for that project, regardless of 64 | ## what source location any dependent projects specify. 65 | # source = "https://github.com/myfork/package.git" 66 | 67 | 68 | [[constraint]] 69 | name = "github.com/dgrijalva/jwt-go" 70 | version = "3.0.0" 71 | 72 | [[constraint]] 73 | name = "github.com/gorilla/handlers" 74 | version = "1.2.1" 75 | 76 | [[constraint]] 77 | name = "github.com/gorilla/mux" 78 | version = "1.4.0" 79 | 80 | [[constraint]] 81 | branch = "master" 82 | name = "github.com/stretchr/testify" 83 | 84 | [[constraint]] 85 | branch = "master" 86 | name = "github.com/axw/gocov" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A simple example of Go (golang) api with jwt authentication 2 | [![CircleCI](https://img.shields.io/circleci/project/pinbar/go-mux-jwt/master.svg)](https://circleci.com/gh/pinbar/go-mux-jwt) 3 | [![Test Coverage](https://codeclimate.com/github/pinbar/go-mux-jwt/badges/coverage.svg)](https://codeclimate.com/github/pinbar/go-mux-jwt/coverage) [![Code Climate](https://codeclimate.com/github/pinbar/go-mux-jwt/badges/gpa.svg)](https://codeclimate.com/github/pinbar/go-mux-jwt) [![Issue Count](https://codeclimate.com/github/pinbar/go-mux-jwt/badges/issue_count.svg)](https://codeclimate.com/github/pinbar/go-mux-jwt) 4 | 5 | ### tech stack 6 | * **Go** - a programming language that is fast, uses minimal resources and supports high concurrency 7 | * **Gorilla Mux** - minimalistic request router and dispatcher 8 | * **Gorilla Logging Handler** - middleware for http request/response logging 9 | * **jwt-go** - a jwt library for Go 10 | * **testify** - testing and assertion library 11 | * **dep** - the official dependency management tool for Go 12 | * **gin (optional)** - livereload utility for faster development turnaround in local 13 | 14 | ### pre-requisites 15 | * Go is installed. To verify, run `go version` 16 | * GOPATH is set (e.g. set to `~/go`) 17 | * PATH includes `GOPATH/bin` 18 | * `dep` is installed 19 | * to get it, run `go get -u github.com/golang/dep/cmd/dep` 20 | 21 | 22 | ### getting started 23 | * clone repo or download zip 24 | * get the dependencies by running `dep ensure -update` in the project directory 25 | * in the project directory, run `go build && ./go-mux-jwt` 26 | * launch the browser and point to the baseurl `localhost:3001` 27 | * port can be changed in `main.go` 28 | * *optional:* 29 | * use **gin** to monitor for changes and automatically restart the application 30 | * if you don't have gin, `go get github.com/codegangsta/gin` 31 | * in the project directory run `gin` (no need to build or run executable, when you do this) 32 | * update `Gopkg.toml` to use different version(s) for the dependency/vendor libs 33 | * run `dep ensure -update` after changing the toml file 34 | 35 | ### running tests 36 | * to run the tests, run `go test` in the project directory 37 | * **test coverage:** 38 | * to run tests and generate coverage report, run `go test -cover`. 39 | * percentage covered is shown in the terminal upon execution of this command 40 | 41 | ### api and authentication scenarios 42 | * access the unsecure api `GET /metacortex` 43 | * all `/api/*` calls are secured with JWT authentication 44 | * try accessing the secure api `GET /api/megacity` to see an auth error 45 | * obtain a JWT token here: `/static/authenticate.html` 46 | * enter programName:programPassword (neo:keanu) 47 | * the response contains a JWT token for that program 48 | * use the token when calling any secure api (`/api/*`): 49 | * set the `Authorization` request header and add the jwt token, like so: 50 | * `Authorization: Bearer \` 51 | * `GET /api/megacity` can be accessed with any valid token but `GET /api/levrai` can only be accessed with neo's token 52 | -------------------------------------------------------------------------------- /authhandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | ) 9 | 10 | func authenticate(w http.ResponseWriter, r *http.Request) { 11 | name := r.FormValue("programName") 12 | password := r.FormValue("programPassword") 13 | 14 | if len(name) == 0 || len(password) == 0 { 15 | w.WriteHeader(http.StatusBadRequest) 16 | w.Write([]byte("Please provide name and password to obtain the token")) 17 | return 18 | } 19 | if (name == "neo" && password == "keanu") || (name == "morpheus" && password == "lawrence") { 20 | token, err := getToken(name) 21 | if err != nil { 22 | w.WriteHeader(http.StatusInternalServerError) 23 | w.Write([]byte("Error generating JWT token: " + err.Error())) 24 | } else { 25 | w.Header().Set("Authorization", "Bearer "+token) 26 | w.WriteHeader(http.StatusOK) 27 | w.Write([]byte("Token: " + token)) 28 | } 29 | } else { 30 | w.WriteHeader(http.StatusUnauthorized) 31 | w.Write([]byte("Name and password do not match")) 32 | return 33 | } 34 | } 35 | 36 | func authMiddleware(next http.Handler) http.Handler { 37 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | 39 | tokenString := r.Header.Get("Authorization") 40 | if len(tokenString) == 0 { 41 | w.WriteHeader(http.StatusUnauthorized) 42 | w.Write([]byte("Missing Authorization Header")) 43 | return 44 | } 45 | tokenString = strings.Replace(tokenString, "Bearer ", "", 1) 46 | claims, err := verifyToken(tokenString) 47 | if err != nil { 48 | w.WriteHeader(http.StatusUnauthorized) 49 | w.Write([]byte("Error verifying JWT token: " + err.Error())) 50 | return 51 | } 52 | name := claims.(jwt.MapClaims)["name"].(string) 53 | role := claims.(jwt.MapClaims)["role"].(string) 54 | 55 | r.Header.Set("name", name) 56 | r.Header.Set("role", role) 57 | 58 | next.ServeHTTP(w, r) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - go test -cover -coverprofile=coverage.out 4 | - go tool cover -html=coverage.out -o coverage.html 5 | post: 6 | - npm install -g codeclimate-test-reporter 7 | - CODECLIMATE_REPO_TOKEN=$CodeClimateToken GOPATH=/home/ubuntu/.go_project codeclimate-test-reporter < coverage.out 8 | - go get -u github.com/jstemmer/go-junit-report 9 | - go test -v 2>&1 | go-junit-report > JUnitReport.xml 10 | - mkdir $CIRCLE_TEST_REPORTS/junitformat 11 | - mv JUnitReport.xml $CIRCLE_TEST_REPORTS/junitformat 12 | - mkdir $CIRCLE_ARTIFACTS/coverage 13 | - mv coverage.html $CIRCLE_ARTIFACTS/coverage -------------------------------------------------------------------------------- /jwtutil.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import jwt "github.com/dgrijalva/jwt-go" 4 | 5 | func getToken(name string) (string, error) { 6 | signingKey := []byte("keymaker") 7 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 8 | "name": name, 9 | "role": "redpill", 10 | }) 11 | tokenString, err := token.SignedString(signingKey) 12 | return tokenString, err 13 | } 14 | 15 | func verifyToken(tokenString string) (jwt.Claims, error) { 16 | signingKey := []byte("keymaker") 17 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 18 | return signingKey, nil 19 | }) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return token.Claims, err 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/gorilla/handlers" 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | func main() { 13 | router := ConfigureRouter() 14 | log.Fatal(http.ListenAndServe(":3001", handlers.LoggingHandler(os.Stdout, router))) 15 | } 16 | 17 | //ConfigureRouter setup the router 18 | func ConfigureRouter() *mux.Router { 19 | router := mux.NewRouter() 20 | 21 | router.PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) 22 | 23 | router.HandleFunc("/", homeHandler) 24 | router.HandleFunc("/metacortex", metacortexHandler) 25 | router.HandleFunc("/agents/{name}", agentsHandler) 26 | 27 | router.HandleFunc("/authenticate", authenticate) 28 | 29 | router.Handle("/api/megacity", authMiddleware(megacityHandler)) 30 | router.Handle("/api/levrai", authMiddleware(levraiHandler)) 31 | 32 | return router 33 | } 34 | 35 | func homeHandler(w http.ResponseWriter, r *http.Request) { 36 | w.Write([]byte("Welcome to the Matrix!")) 37 | } 38 | func metacortexHandler(w http.ResponseWriter, r *http.Request) { 39 | w.WriteHeader(http.StatusOK) 40 | w.Write([]byte("Mr Anderson's not so secure workplace!")) 41 | } 42 | func agentsHandler(w http.ResponseWriter, r *http.Request) { 43 | vars := mux.Vars(r) 44 | w.WriteHeader(http.StatusOK) 45 | w.Write([]byte("My name is agent " + vars["name"])) 46 | } 47 | 48 | var megacityHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | w.Write([]byte("Welcome to the Megacity!")) 50 | }) 51 | 52 | var levraiHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 | name := r.Header.Get("name") 54 | if name != "neo" { 55 | w.WriteHeader(http.StatusUnauthorized) 56 | w.Write([]byte("Only Neo can enter the Merovingian's restaurant!")) 57 | return 58 | } 59 | w.Write([]byte("Welcome to the LeVrai!")) 60 | }) 61 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestHome(t *testing.T) { 15 | responseStatus, responseBody := testHelperProcessRequest("/", "", t) 16 | assert.Equal(t, http.StatusOK, responseStatus, "should return status OK") 17 | assert.Equal(t, "Welcome to the Matrix!", responseBody, "wrong message") 18 | } 19 | func TestMetacortex(t *testing.T) { 20 | responseStatus, responseBody := testHelperProcessRequest("/metacortex", "", t) 21 | assert.Equal(t, http.StatusOK, responseStatus, "should return status OK") 22 | assert.Equal(t, "Mr Anderson's not so secure workplace!", responseBody, "wrong message") 23 | } 24 | func TestAgentName(t *testing.T) { 25 | responseStatus, responseBody := testHelperProcessRequest("/agents/smith", "", t) 26 | assert.Equal(t, http.StatusOK, responseStatus, "should return status OK") 27 | assert.Equal(t, "My name is agent smith", string(responseBody), "wrong message") 28 | } 29 | func TestAuthenticateNoCreds(t *testing.T) { 30 | responseStatus, responseBody := testHelperAuthenticate("", "", t) 31 | assert.Equal(t, http.StatusBadRequest, responseStatus, "should return status 400") 32 | assert.Equal(t, "Please provide name and password to obtain the token", string(responseBody), "wrong password") 33 | } 34 | func TestAuthenticateBadCreds(t *testing.T) { 35 | responseStatus, responseBody := testHelperAuthenticate("neo", "lawrence", t) 36 | assert.Equal(t, http.StatusUnauthorized, responseStatus, "should return status 401") 37 | assert.Equal(t, "Name and password do not match", string(responseBody), "wrong password") 38 | } 39 | func TestAuthenticateNeoCreds(t *testing.T) { 40 | responseStatus, responseBody := testHelperAuthenticate("neo", "keanu", t) 41 | assert.Equal(t, http.StatusOK, responseStatus, "should return status 200") 42 | assert.Equal(t, "Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibmVvIiwicm9sZSI6InJlZHBpbGwifQ.TS72DgJx5euYy-YXVEPoHt9Pl0Y7YpV4tRecQaxv7Xk", string(responseBody), "token not created") 43 | } 44 | func TestAuthenticateMorpheusCreds(t *testing.T) { 45 | responseStatus, responseBody := testHelperAuthenticate("morpheus", "lawrence", t) 46 | assert.Equal(t, http.StatusOK, responseStatus, "should return status 200") 47 | assert.Equal(t, "Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibW9ycGhldXMiLCJyb2xlIjoicmVkcGlsbCJ9.zNV3twzLyIv0GeIx4BhcVM0dtlC73izClB0XIlZoxz4", string(responseBody), "token not created") 48 | } 49 | func TestMegacityNoAuth(t *testing.T) { 50 | responseStatus, responseBody := testHelperProcessRequest("/api/megacity", "", t) 51 | assert.Equal(t, http.StatusUnauthorized, responseStatus, "should return status 401") 52 | assert.Equal(t, "Missing Authorization Header", string(responseBody), "no token should return unauthorized error") 53 | } 54 | func TestMegacityWithAuth(t *testing.T) { 55 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibmVvIiwicm9sZSI6InJlZHBpbGwifQ.TS72DgJx5euYy-YXVEPoHt9Pl0Y7YpV4tRecQaxv7Xk" 56 | responseStatus, responseBody := testHelperProcessRequest("/api/megacity", token, t) 57 | assert.Equal(t, http.StatusOK, responseStatus, "should return status 200") 58 | assert.Equal(t, "Welcome to the Megacity!", string(responseBody), "request with token failed") 59 | } 60 | func TestMegacityWithAuthBadToken(t *testing.T) { 61 | token := "some.bad.token" 62 | responseStatus, responseBody := testHelperProcessRequest("/api/megacity", token, t) 63 | assert.Equal(t, http.StatusUnauthorized, responseStatus, "should return status 401") 64 | assert.Equal(t, "Error verifying JWT token: invalid character '²' looking for beginning of value", string(responseBody), "request with bad token should return unauthorized error") 65 | } 66 | func TestLeVraiWithNeoAuth(t *testing.T) { 67 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibmVvIiwicm9sZSI6InJlZHBpbGwifQ.TS72DgJx5euYy-YXVEPoHt9Pl0Y7YpV4tRecQaxv7Xk" 68 | responseStatus, responseBody := testHelperProcessRequest("/api/levrai", token, t) 69 | assert.Equal(t, http.StatusOK, responseStatus, "should return status 200") 70 | assert.Equal(t, "Welcome to the LeVrai!", string(responseBody), "request should have neo token") 71 | } 72 | func TestLeVraiWithMorpheusAuth(t *testing.T) { 73 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibW9ycGhldXMiLCJyb2xlIjoicmVkcGlsbCJ9.zNV3twzLyIv0GeIx4BhcVM0dtlC73izClB0XIlZoxz4" 74 | responseStatus, responseBody := testHelperProcessRequest("/api/levrai", token, t) 75 | assert.Equal(t, http.StatusUnauthorized, responseStatus, "should return status 401") 76 | assert.Equal(t, "Only Neo can enter the Merovingian's restaurant!", string(responseBody), "request should have neo token") 77 | } 78 | 79 | //test helper functions 80 | func testHelperProcessRequest(reqURI string, jwtToken string, t *testing.T) (int, string) { 81 | r := ConfigureRouter() 82 | ts := httptest.NewServer(r) 83 | defer ts.Close() 84 | 85 | reqURL := ts.URL + reqURI 86 | req, _ := http.NewRequest("GET", reqURL, nil) 87 | if len(jwtToken) > 0 { 88 | 89 | req.Header.Set("Authorization", "Bearer "+jwtToken) 90 | } 91 | client := &http.Client{} 92 | resp, err := client.Do(req) 93 | 94 | if err != nil { 95 | t.Error("Error making the request") 96 | } 97 | responseBody, err := ioutil.ReadAll(resp.Body) 98 | resp.Body.Close() 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | 103 | return resp.StatusCode, string(responseBody) 104 | } 105 | 106 | func testHelperAuthenticate(programName string, programPassword string, t *testing.T) (int, string) { 107 | r := ConfigureRouter() 108 | ts := httptest.NewServer(r) 109 | defer ts.Close() 110 | 111 | reqURL := ts.URL + "/authenticate" 112 | 113 | var data = make(url.Values) 114 | data.Set("programName", programName) 115 | data.Set("programPassword", programPassword) 116 | 117 | resp, err := http.PostForm(reqURL, data) 118 | if err != nil { 119 | t.Error("Error making the request") 120 | } 121 | responseBody, err := ioutil.ReadAll(resp.Body) 122 | resp.Body.Close() 123 | if err != nil { 124 | log.Fatal(err) 125 | } 126 | 127 | return resp.StatusCode, string(responseBody) 128 | } 129 | -------------------------------------------------------------------------------- /static/authenticate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
Authenticate to make secure api calls!
5 |
6 |
7 | Program Name: 8 |
9 | Program Password: 10 |
11 |
12 | 13 |
14 | 15 | --------------------------------------------------------------------------------