package-installer-cli 1.3.3 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/dist/commands/email.js +1150 -0
- data/dist/email-templates/bugReport.js +108 -0
- data/dist/email-templates/collectors/bugReport.js +41 -0
- data/dist/email-templates/collectors/common.js +56 -0
- data/dist/email-templates/collectors/customMessage.js +48 -0
- data/dist/email-templates/collectors/docs.js +41 -0
- data/dist/email-templates/collectors/featureRequest.js +47 -0
- data/dist/email-templates/collectors/improvement.js +52 -0
- data/dist/email-templates/collectors/index.js +9 -0
- data/dist/email-templates/collectors/question.js +31 -0
- data/dist/email-templates/collectors/templateRequest.js +53 -0
- data/dist/email-templates/customMessage.js +135 -0
- data/dist/email-templates/docs.js +111 -0
- data/dist/email-templates/featureRequest.js +111 -0
- data/dist/email-templates/generator.js +67 -0
- data/dist/email-templates/improvement.js +121 -0
- data/dist/email-templates/index.js +15 -0
- data/dist/email-templates/question.js +101 -0
- data/dist/email-templates/styles.js +168 -0
- data/dist/email-templates/templateRequest.js +120 -0
- data/dist/email-templates/testEmail.js +56 -0
- data/dist/email-templates/types.js +1 -0
- data/dist/index.js +29 -1
- data/dist/utils/featureInstaller.js +4 -11
- data/features/ai/ai.json +547 -0
- data/features/analytics/analytics.json +4 -0
- data/features/auth/auth.json +473 -0
- data/features/auth/auth0/django/backend/apiexample/urls.py +10 -0
- data/features/auth/auth0/django/backend/apiexample/validator.py +21 -0
- data/features/auth/auth0/django/backend/apiexample/views.py +30 -0
- data/features/auth/auth0/django/backend/requirements.txt +4 -0
- data/features/auth/auth0/django/web-app/requirements.txt +4 -0
- data/features/auth/auth0/django/web-app/webappexample/settings.py +28 -0
- data/features/auth/auth0/django/web-app/webappexample/templates/index.html +16 -0
- data/features/auth/auth0/django/web-app/webappexample/urls.py +10 -0
- data/features/auth/auth0/django/web-app/webappexample/views.py +52 -0
- data/features/auth/auth0/go/backend/go.mod +8 -0
- data/features/auth/auth0/go/backend/main.go +60 -0
- data/features/auth/auth0/go/backend/middleware/jwt.go +81 -0
- data/features/auth/auth0/go/web-app/auth.go +56 -0
- data/features/auth/auth0/go/web-app/callback.go +52 -0
- data/features/auth/auth0/go/web-app/go.mod +11 -0
- data/features/auth/auth0/go/web-app/isAuthenticated.go +20 -0
- data/features/auth/auth0/go/web-app/login.go +47 -0
- data/features/auth/auth0/go/web-app/logout.go +38 -0
- data/features/auth/auth0/go/web-app/main.go +31 -0
- data/features/auth/auth0/go/web-app/router.go +44 -0
- data/features/auth/auth0/go/web-app/user.go +18 -0
- data/features/auth/auth0/ruby-on-rails/backend/Gemfile +1 -0
- data/features/auth/auth0/ruby-on-rails/backend/app/controllers/application_controller.rb +5 -0
- data/features/auth/auth0/ruby-on-rails/backend/app/controllers/concern/secured.rb +60 -0
- data/features/auth/auth0/ruby-on-rails/backend/app/controllers/private_controller.rb +6 -0
- data/features/auth/auth0/ruby-on-rails/backend/app/controllers/public-controller.rb +6 -0
- data/features/auth/auth0/ruby-on-rails/backend/app/lib/auth0_client.rb +59 -0
- data/features/auth/auth0/ruby-on-rails/web-app/Gemfile +2 -0
- data/features/auth/auth0/ruby-on-rails/web-app/auth0_controller.rb +41 -0
- data/features/auth/auth0/ruby-on-rails/web-app/config/auth0.yml +4 -0
- data/features/auth/auth0/ruby-on-rails/web-app/config/initializers/auth0.rb +14 -0
- data/features/auth/auth0/ruby-on-rails/web-app/config/routes.rb +6 -0
- data/features/auth/auth0/ruby-on-rails/web-app/config/secured.rb +11 -0
- data/features/auth/clerk/go/clerk_client.go +28 -0
- data/features/auth/clerk/go/go.mod +5 -0
- data/features/auth/clerk/go/main.go +82 -0
- data/features/auth/clerk/nextjs/typescript/app/layout.tsx +2 -2
- data/features/auth/clerk/ruby-on-rails/Gemfile +1 -0
- data/features/auth/clerk/ruby-on-rails/app.rb +50 -0
- data/features/auth/clerk/ruby-on-rails/config/initializers/clerk.rb +4 -0
- data/features/aws/aws.json +5207 -0
- data/features/database/database.json +246 -0
- data/features/docker/docker.json +108 -0
- data/features/features.json +44 -7321
- data/features/gitignore/gitignore.json +61 -0
- data/features/monitoring/monitoring.json +5 -0
- data/features/payment/payment.json +347 -0
- data/features/storage/storage.json +371 -0
- data/features/testing/jest/angularjs/typescript/tests/angularjs.test.ts +12 -0
- data/features/testing/jest/expressjs/javascript/tests/expressjs.test.js +10 -0
- data/features/testing/jest/expressjs/typescript/tests/expressjs.test.ts +10 -0
- data/features/testing/jest/nestjs/typescript/tests/nestjs.test.ts +10 -0
- data/features/testing/jest/nextjs/javascript/tests/nextjs.test.js +10 -0
- data/features/testing/jest/nextjs/typescript/tests/nextjs.test.ts +10 -0
- data/features/testing/jest/nuxtjs/typescript/tests/nuxtjs.test.ts +9 -0
- data/features/testing/jest/reactjs/javascript/tests/reactjs.test.js +10 -0
- data/features/testing/jest/reactjs/typescript/tests/reactjs.test.ts +10 -0
- data/features/testing/jest/reactjs-expressjs-shadcn/javascript/tests/reactjs.expressjs.shadcn.test.js +16 -0
- data/features/testing/jest/reactjs-expressjs-shadcn/typescript/tests/reactjs.expressjs.shadcn.test.ts +16 -0
- data/features/testing/jest/reactjs-nestjs-shadcn/typescript/tests/reactjs.nestjs.shadcn.test.ts +14 -0
- data/features/testing/jest/remixjs/typescript/tests/remixjs.test.ts +9 -0
- data/features/testing/jest/vuejs/javascript/tests/vuejs.test.ts +9 -0
- data/features/testing/jest/vuejs/typescript/tests/vuejs.test.ts +9 -0
- data/features/testing/testing.json +102 -0
- data/features/ui/ui.json +91 -0
- metadata +90 -8
- data/features/testing/jest/angularjs/tests/angularjs.test.js +0 -12
- data/features/testing/jest/expressjs/tests/javascript/expressjs.test.js +0 -12
- data/features/testing/jest/expressjs/tests/typescript/expressjs.test.ts +0 -12
- data/features/testing/jest/go/tests/go.test.js +0 -12
- data/features/testing/jest/nextjs/tests/javascript/nextjs.test.js +0 -12
- data/features/testing/jest/nextjs/tests/typescript/nextjs.test.ts +0 -12
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"01-Authorization-RS256/middleware"
|
|
5
|
+
"log"
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
jwtmiddleware "github.com/auth0/go-jwt-middleware/v2"
|
|
9
|
+
"github.com/auth0/go-jwt-middleware/v2/validator"
|
|
10
|
+
"github.com/joho/godotenv"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func main() {
|
|
14
|
+
if err := godotenv.Load(); err != nil {
|
|
15
|
+
log.Fatalf("Error loading the .env file: %v", err)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
router := http.NewServeMux()
|
|
19
|
+
|
|
20
|
+
// This route is always accessible.
|
|
21
|
+
router.Handle("/api/public", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
22
|
+
w.Header().Set("Content-Type", "application/json")
|
|
23
|
+
w.WriteHeader(http.StatusOK)
|
|
24
|
+
w.Write([]byte(`{"message":"Hello from a public endpoint! You don't need to be authenticated to see this."}`))
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
// This route is only accessible if the user has a valid access_token.
|
|
28
|
+
router.Handle("/api/private", middleware.EnsureValidToken()(
|
|
29
|
+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
30
|
+
w.Header().Set("Content-Type", "application/json")
|
|
31
|
+
w.WriteHeader(http.StatusOK)
|
|
32
|
+
w.Write([]byte(`{"message":"Hello from a private endpoint! You need to be authenticated to see this."}`))
|
|
33
|
+
}),
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
// This route is only accessible if the user has a
|
|
37
|
+
// valid access_token with the read:messages scope.
|
|
38
|
+
router.Handle("/api/private-scoped", middleware.EnsureValidToken()(
|
|
39
|
+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
40
|
+
w.Header().Set("Content-Type", "application/json")
|
|
41
|
+
|
|
42
|
+
token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
|
|
43
|
+
|
|
44
|
+
claims := token.CustomClaims.(*middleware.CustomClaims)
|
|
45
|
+
if !claims.HasScope("read:messages") {
|
|
46
|
+
w.WriteHeader(http.StatusForbidden)
|
|
47
|
+
w.Write([]byte(`{"message":"Insufficient scope."}`))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
w.WriteHeader(http.StatusOK)
|
|
52
|
+
w.Write([]byte(`{"message":"Hello from a private endpoint! You need to be authenticated to see this."}`))
|
|
53
|
+
}),
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
log.Print("Server listening on http://localhost:3010")
|
|
57
|
+
if err := http.ListenAndServe("0.0.0.0:3010", router); err != nil {
|
|
58
|
+
log.Fatalf("There was an error with the http server: %v", err)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package middleware
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"log"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/url"
|
|
8
|
+
"os"
|
|
9
|
+
"strings"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
jwtmiddleware "github.com/auth0/go-jwt-middleware/v2"
|
|
13
|
+
"github.com/auth0/go-jwt-middleware/v2/jwks"
|
|
14
|
+
"github.com/auth0/go-jwt-middleware/v2/validator"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// CustomClaims contains custom data we want from the token.
|
|
18
|
+
type CustomClaims struct {
|
|
19
|
+
Scope string `json:"scope"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Validate does nothing for this example, but we need
|
|
23
|
+
// it to satisfy validator.CustomClaims interface.
|
|
24
|
+
func (c CustomClaims) Validate(ctx context.Context) error {
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// EnsureValidToken is a middleware that will check the validity of our JWT.
|
|
29
|
+
func EnsureValidToken() func(next http.Handler) http.Handler {
|
|
30
|
+
issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/")
|
|
31
|
+
if err != nil {
|
|
32
|
+
log.Fatalf("Failed to parse the issuer url: %v", err)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
|
|
36
|
+
|
|
37
|
+
jwtValidator, err := validator.New(
|
|
38
|
+
provider.KeyFunc,
|
|
39
|
+
validator.RS256,
|
|
40
|
+
issuerURL.String(),
|
|
41
|
+
[]string{os.Getenv("AUTH0_AUDIENCE")},
|
|
42
|
+
validator.WithCustomClaims(
|
|
43
|
+
func() validator.CustomClaims {
|
|
44
|
+
return &CustomClaims{}
|
|
45
|
+
},
|
|
46
|
+
),
|
|
47
|
+
validator.WithAllowedClockSkew(time.Minute),
|
|
48
|
+
)
|
|
49
|
+
if err != nil {
|
|
50
|
+
log.Fatalf("Failed to set up the jwt validator")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
|
|
54
|
+
log.Printf("Encountered error while validating JWT: %v", err)
|
|
55
|
+
|
|
56
|
+
w.Header().Set("Content-Type", "application/json")
|
|
57
|
+
w.WriteHeader(http.StatusUnauthorized)
|
|
58
|
+
w.Write([]byte(`{"message":"Failed to validate JWT."}`))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
middleware := jwtmiddleware.New(
|
|
62
|
+
jwtValidator.ValidateToken,
|
|
63
|
+
jwtmiddleware.WithErrorHandler(errorHandler),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return func(next http.Handler) http.Handler {
|
|
67
|
+
return middleware.CheckJWT(next)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// HasScope checks whether our claims have a specific scope.
|
|
72
|
+
func (c CustomClaims) HasScope(expectedScope string) bool {
|
|
73
|
+
result := strings.Split(c.Scope, " ")
|
|
74
|
+
for i := range result {
|
|
75
|
+
if result[i] == expectedScope {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Save this file in ./platform/authenticator/auth.go
|
|
2
|
+
|
|
3
|
+
package authenticator
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"errors"
|
|
8
|
+
"os"
|
|
9
|
+
|
|
10
|
+
"github.com/coreos/go-oidc/v3/oidc"
|
|
11
|
+
"golang.org/x/oauth2"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Authenticator is used to authenticate our users.
|
|
15
|
+
type Authenticator struct {
|
|
16
|
+
*oidc.Provider
|
|
17
|
+
oauth2.Config
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// New instantiates the *Authenticator.
|
|
21
|
+
func New() (*Authenticator, error) {
|
|
22
|
+
provider, err := oidc.NewProvider(
|
|
23
|
+
context.Background(),
|
|
24
|
+
"https://"+os.Getenv("AUTH0_DOMAIN")+"/",
|
|
25
|
+
)
|
|
26
|
+
if err != nil {
|
|
27
|
+
return nil, err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
conf := oauth2.Config{
|
|
31
|
+
ClientID: os.Getenv("AUTH0_CLIENT_ID"),
|
|
32
|
+
ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"),
|
|
33
|
+
RedirectURL: os.Getenv("AUTH0_CALLBACK_URL"),
|
|
34
|
+
Endpoint: provider.Endpoint(),
|
|
35
|
+
Scopes: []string{oidc.ScopeOpenID, "profile"},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return &Authenticator{
|
|
39
|
+
Provider: provider,
|
|
40
|
+
Config: conf,
|
|
41
|
+
}, nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken.
|
|
45
|
+
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
|
|
46
|
+
rawIDToken, ok := token.Extra("id_token").(string)
|
|
47
|
+
if !ok {
|
|
48
|
+
return nil, errors.New("no id_token field in oauth2 token")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
oidcConfig := &oidc.Config{
|
|
52
|
+
ClientID: a.ClientID,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/ Save this file in ./web/app/callback/callback.go
|
|
2
|
+
|
|
3
|
+
package callback
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
"github.com/gin-contrib/sessions"
|
|
9
|
+
"github.com/gin-gonic/gin"
|
|
10
|
+
|
|
11
|
+
"01-Login/platform/authenticator"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Handler for our callback.
|
|
15
|
+
func Handler(auth *authenticator.Authenticator) gin.HandlerFunc {
|
|
16
|
+
return func(ctx *gin.Context) {
|
|
17
|
+
session := sessions.Default(ctx)
|
|
18
|
+
if ctx.Query("state") != session.Get("state") {
|
|
19
|
+
ctx.String(http.StatusBadRequest, "Invalid state parameter.")
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Exchange an authorization code for a token.
|
|
24
|
+
token, err := auth.Exchange(ctx.Request.Context(), ctx.Query("code"))
|
|
25
|
+
if err != nil {
|
|
26
|
+
ctx.String(http.StatusUnauthorized, "Failed to exchange an authorization code for a token.")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
idToken, err := auth.VerifyIDToken(ctx.Request.Context(), token)
|
|
31
|
+
if err != nil {
|
|
32
|
+
ctx.String(http.StatusInternalServerError, "Failed to verify ID Token.")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var profile map[string]interface{}
|
|
37
|
+
if err := idToken.Claims(&profile); err != nil {
|
|
38
|
+
ctx.String(http.StatusInternalServerError, err.Error())
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
session.Set("access_token", token.AccessToken)
|
|
43
|
+
session.Set("profile", profile)
|
|
44
|
+
if err := session.Save(); err != nil {
|
|
45
|
+
ctx.String(http.StatusInternalServerError, err.Error())
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Redirect to logged in page.
|
|
50
|
+
ctx.Redirect(http.StatusTemporaryRedirect, "/user")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Save this file in ./platform/middleware/isAuthenticated.go
|
|
2
|
+
|
|
3
|
+
package middleware
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
"github.com/gin-contrib/sessions"
|
|
9
|
+
"github.com/gin-gonic/gin"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// IsAuthenticated is a middleware that checks if
|
|
13
|
+
// the user has already been authenticated previously.
|
|
14
|
+
func IsAuthenticated(ctx *gin.Context) {
|
|
15
|
+
if sessions.Default(ctx).Get("profile") == nil {
|
|
16
|
+
ctx.Redirect(http.StatusSeeOther, "/")
|
|
17
|
+
} else {
|
|
18
|
+
ctx.Next()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Save this file in ./web/app/login/login.go
|
|
2
|
+
|
|
3
|
+
package login
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"crypto/rand"
|
|
7
|
+
"encoding/base64"
|
|
8
|
+
"net/http"
|
|
9
|
+
|
|
10
|
+
"github.com/gin-contrib/sessions"
|
|
11
|
+
"github.com/gin-gonic/gin"
|
|
12
|
+
|
|
13
|
+
"01-Login/platform/authenticator"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// Handler for our login.
|
|
17
|
+
func Handler(auth *authenticator.Authenticator) gin.HandlerFunc {
|
|
18
|
+
return func(ctx *gin.Context) {
|
|
19
|
+
state, err := generateRandomState()
|
|
20
|
+
if err != nil {
|
|
21
|
+
ctx.String(http.StatusInternalServerError, err.Error())
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Save the state inside the session.
|
|
26
|
+
session := sessions.Default(ctx)
|
|
27
|
+
session.Set("state", state)
|
|
28
|
+
if err := session.Save(); err != nil {
|
|
29
|
+
ctx.String(http.StatusInternalServerError, err.Error())
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ctx.Redirect(http.StatusTemporaryRedirect, auth.AuthCodeURL(state))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func generateRandomState() (string, error) {
|
|
38
|
+
b := make([]byte, 32)
|
|
39
|
+
_, err := rand.Read(b)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return "", err
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
state := base64.StdEncoding.EncodeToString(b)
|
|
45
|
+
|
|
46
|
+
return state, nil
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Save this file in ./web/app/logout/logout.go
|
|
2
|
+
|
|
3
|
+
package logout
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/url"
|
|
8
|
+
"os"
|
|
9
|
+
|
|
10
|
+
"github.com/gin-gonic/gin"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// Handler for our logout.
|
|
14
|
+
func Handler(ctx *gin.Context) {
|
|
15
|
+
logoutUrl, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/v2/logout")
|
|
16
|
+
if err != nil {
|
|
17
|
+
ctx.String(http.StatusInternalServerError, err.Error())
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
scheme := "http"
|
|
22
|
+
if ctx.Request.TLS != nil {
|
|
23
|
+
scheme = "https"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
returnTo, err := url.Parse(scheme + "://" + ctx.Request.Host)
|
|
27
|
+
if err != nil {
|
|
28
|
+
ctx.String(http.StatusInternalServerError, err.Error())
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
parameters := url.Values{}
|
|
33
|
+
parameters.Add("returnTo", returnTo.String())
|
|
34
|
+
parameters.Add("client_id", os.Getenv("AUTH0_CLIENT_ID"))
|
|
35
|
+
logoutUrl.RawQuery = parameters.Encode()
|
|
36
|
+
|
|
37
|
+
ctx.Redirect(http.StatusTemporaryRedirect, logoutUrl.String())
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Save this file in ./main.go
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"log"
|
|
7
|
+
"net/http"
|
|
8
|
+
|
|
9
|
+
"github.com/joho/godotenv"
|
|
10
|
+
|
|
11
|
+
"01-Login/platform/authenticator"
|
|
12
|
+
"01-Login/platform/router"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func main() {
|
|
16
|
+
if err := godotenv.Load(); err != nil {
|
|
17
|
+
log.Fatalf("Failed to load the env vars: %v", err)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
auth, err := authenticator.New()
|
|
21
|
+
if err != nil {
|
|
22
|
+
log.Fatalf("Failed to initialize the authenticator: %v", err)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
rtr := router.New(auth)
|
|
26
|
+
|
|
27
|
+
log.Print("Server listening on http://localhost:3000/")
|
|
28
|
+
if err := http.ListenAndServe("0.0.0.0:3000", rtr); err != nil {
|
|
29
|
+
log.Fatalf("There was an error with the http server: %v", err)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Save this file in ./platform/router/router.go
|
|
2
|
+
|
|
3
|
+
package router
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"encoding/gob"
|
|
7
|
+
"net/http"
|
|
8
|
+
|
|
9
|
+
"github.com/gin-contrib/sessions"
|
|
10
|
+
"github.com/gin-contrib/sessions/cookie"
|
|
11
|
+
"github.com/gin-gonic/gin"
|
|
12
|
+
|
|
13
|
+
"01-Login/platform/authenticator"
|
|
14
|
+
"01-Login/platform/middleware"
|
|
15
|
+
"01-Login/web/app/callback"
|
|
16
|
+
"01-Login/web/app/login"
|
|
17
|
+
"01-Login/web/app/logout"
|
|
18
|
+
"01-Login/web/app/user"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// New registers the routes and returns the router.
|
|
22
|
+
func New(auth *authenticator.Authenticator) *gin.Engine {
|
|
23
|
+
router := gin.Default()
|
|
24
|
+
|
|
25
|
+
// To store custom types in our cookies,
|
|
26
|
+
// we must first register them using gob.Register
|
|
27
|
+
gob.Register(map[string]interface{}{})
|
|
28
|
+
|
|
29
|
+
store := cookie.NewStore([]byte("secret"))
|
|
30
|
+
router.Use(sessions.Sessions("auth-session", store))
|
|
31
|
+
|
|
32
|
+
router.Static("/public", "web/static")
|
|
33
|
+
router.LoadHTMLGlob("web/template/*")
|
|
34
|
+
|
|
35
|
+
router.GET("/", func(ctx *gin.Context) {
|
|
36
|
+
ctx.HTML(http.StatusOK, "home.html", nil)
|
|
37
|
+
})
|
|
38
|
+
router.GET("/login", login.Handler(auth))
|
|
39
|
+
router.GET("/callback", callback.Handler(auth))
|
|
40
|
+
router.GET("/user", user.Handler)
|
|
41
|
+
router.GET("/logout", logout.Handler)
|
|
42
|
+
|
|
43
|
+
return router
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Save this file in ./web/app/user/user.go
|
|
2
|
+
|
|
3
|
+
package user
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
"github.com/gin-contrib/sessions"
|
|
9
|
+
"github.com/gin-gonic/gin"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// Handler for our logged-in user page.
|
|
13
|
+
func Handler(ctx *gin.Context) {
|
|
14
|
+
session := sessions.Default(ctx)
|
|
15
|
+
profile := session.Get("profile")
|
|
16
|
+
|
|
17
|
+
ctx.HTML(http.StatusOK, "user.html", profile)
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gem 'jwt'
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Secured
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
REQUIRES_AUTHENTICATION = { message: 'Requires authentication' }.freeze
|
|
7
|
+
BAD_CREDENTIALS = {
|
|
8
|
+
message: 'Bad credentials'
|
|
9
|
+
}.freeze
|
|
10
|
+
MALFORMED_AUTHORIZATION_HEADER = {
|
|
11
|
+
error: 'invalid_request',
|
|
12
|
+
error_description: 'Authorization header value must follow this format: Bearer access-token',
|
|
13
|
+
message: 'Bad credentials'
|
|
14
|
+
}.freeze
|
|
15
|
+
INSUFFICIENT_PERMISSIONS = {
|
|
16
|
+
error: 'insufficient_permissions',
|
|
17
|
+
error_description: 'The access token does not contain the required permissions',
|
|
18
|
+
message: 'Permission denied'
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def authorize
|
|
22
|
+
token = token_from_request
|
|
23
|
+
|
|
24
|
+
return if performed?
|
|
25
|
+
|
|
26
|
+
validation_response = Auth0Client.validate_token(token)
|
|
27
|
+
|
|
28
|
+
@decoded_token = validation_response.decoded_token
|
|
29
|
+
|
|
30
|
+
return unless (error = validation_response.error)
|
|
31
|
+
|
|
32
|
+
render json: { message: error.message }, status: error.status
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_permissions(permissions)
|
|
36
|
+
raise 'validate_permissions needs to be called with a block' unless block_given?
|
|
37
|
+
return yield if @decoded_token.validate_permissions(permissions)
|
|
38
|
+
|
|
39
|
+
render json: INSUFFICIENT_PERMISSIONS, status: :forbidden
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def token_from_request
|
|
45
|
+
authorization_header_elements = request.headers['Authorization']&.split
|
|
46
|
+
|
|
47
|
+
render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements
|
|
48
|
+
|
|
49
|
+
unless authorization_header_elements.length == 2
|
|
50
|
+
render json: MALFORMED_AUTHORIZATION_HEADER,
|
|
51
|
+
status: :unauthorized and return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
scheme, token = authorization_header_elements
|
|
55
|
+
|
|
56
|
+
render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer'
|
|
57
|
+
|
|
58
|
+
token
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jwt'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
|
|
6
|
+
# Auth0Client class to handle JWT token validation
|
|
7
|
+
class Auth0Client
|
|
8
|
+
# Auth0 Client Objects
|
|
9
|
+
Error = Struct.new(:message, :status)
|
|
10
|
+
Response = Struct.new(:decoded_token, :error)
|
|
11
|
+
Token = Struct.new(:token) do
|
|
12
|
+
def validate_permissions(permissions)
|
|
13
|
+
required_permissions = Set.new permissions
|
|
14
|
+
scopes = token[0]['scope']
|
|
15
|
+
token_permissions = scopes.present? ? Set.new(scopes.split(" ")) : Set.new
|
|
16
|
+
required_permissions <= token_permissions
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Helper Functions
|
|
21
|
+
def self.domain_url
|
|
22
|
+
"https://#{Rails.configuration.auth0.domain}/"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.decode_token(token, jwks_hash)
|
|
26
|
+
JWT.decode(token, nil, true, {
|
|
27
|
+
algorithm: 'RS256',
|
|
28
|
+
iss: domain_url,
|
|
29
|
+
verify_iss: true,
|
|
30
|
+
aud: Rails.configuration.auth0.audience,
|
|
31
|
+
verify_aud: true,
|
|
32
|
+
jwks: { keys: jwks_hash[:keys] }
|
|
33
|
+
})
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.get_jwks
|
|
37
|
+
jwks_uri = URI("#{domain_url}.well-known/jwks.json")
|
|
38
|
+
Net::HTTP.get_response jwks_uri
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Token Validation
|
|
42
|
+
def self.validate_token(token)
|
|
43
|
+
jwks_response = get_jwks
|
|
44
|
+
|
|
45
|
+
unless jwks_response.is_a? Net::HTTPSuccess
|
|
46
|
+
error = Error.new(message: 'Unable to verify credentials', status: :internal_server_error)
|
|
47
|
+
return Response.new(nil, error)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
jwks_hash = JSON.parse(jwks_response.body).deep_symbolize_keys
|
|
51
|
+
|
|
52
|
+
decoded_token = decode_token(token, jwks_hash)
|
|
53
|
+
|
|
54
|
+
Response.new(Token.new(decoded_token), nil)
|
|
55
|
+
rescue JWT::VerificationError, JWT::DecodeError => e
|
|
56
|
+
error = Error.new('Bad credentials', :unauthorized)
|
|
57
|
+
Response.new(nil, error)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class Auth0Controller < ApplicationController
|
|
2
|
+
def callback
|
|
3
|
+
# OmniAuth stores the informatin returned from
|
|
4
|
+
# Auth0 and the IdP in request.env['omniauth.auth'].
|
|
5
|
+
# In this code, you will pull the raw_info supplied
|
|
6
|
+
# from the id_token and assign it to the session.
|
|
7
|
+
# Refer to https://github.com/auth0/omniauth-auth0/blob/master/EXAMPLES.md#example-of-the-resulting-authentication-hash
|
|
8
|
+
# for complete information on 'omniauth.auth' contents.
|
|
9
|
+
auth_info = request.env['omniauth.auth']
|
|
10
|
+
session[:userinfo] = auth_info['extra']['raw_info']
|
|
11
|
+
|
|
12
|
+
# Redirect to the URL you want after successful auth
|
|
13
|
+
redirect_to '/dashboard'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def failure
|
|
17
|
+
# Handles failed authentication
|
|
18
|
+
@error_msg = request.params['message']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def logout
|
|
22
|
+
reset_session
|
|
23
|
+
redirect_to logout_url
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
AUTH0_CONFIG = Rails.application.config_for(:auth0)
|
|
28
|
+
|
|
29
|
+
def logout_url
|
|
30
|
+
request_params = {
|
|
31
|
+
returnTo: root_url,
|
|
32
|
+
client_id: AUTH0_CONFIG['auth0_client_id']
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
URI::HTTPS.build(host: AUTH0_CONFIG['auth0_domain'], path: '/v2/logout', query: to_query(request_params)).to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_query(hash)
|
|
39
|
+
hash.map { |k, v| "#{k}=#{CGI.escape(v)}" unless v.nil? }.reject(&:nil?).join('&')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
AUTH0_CONFIG = Rails.application.config_for(:auth0)
|
|
2
|
+
|
|
3
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
4
|
+
provider(
|
|
5
|
+
:auth0,
|
|
6
|
+
AUTH0_CONFIG['auth0_client_id'],
|
|
7
|
+
AUTH0_CONFIG['auth0_client_secret'],
|
|
8
|
+
AUTH0_CONFIG['auth0_domain'],
|
|
9
|
+
callback_path: '/auth/auth0/callback',
|
|
10
|
+
authorize_params: {
|
|
11
|
+
scope: 'openid profile'
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
end
|