Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ory Kratos Integration. #198

Merged
merged 12 commits into from
Feb 5, 2024
Merged
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,18 @@ SQLITE_FILEPATH=database/golang-api.db

#JWT
JWT_SECRET=ThisIsKey

#KRATOS
KRATOS_ENABLED=true
KRATOS_COOKIE_EXPIRATION_TIME=2h23m # Time should be in the format of 1h23m, valid units are "h", "m", "s", "ms", "us", "ns"
SERVE_PUBLIC_BASE_URL=http://127.0.0.1:4433/ # URL where the endpoint is exposed at. **used to generate redirects.
SERVE_PUBLIC_HOST=127.0.0.1 # host on which the public endpoint listenes on.
SERVE_PUBLIC_PORT=4435 # port of the kratos public endpoint.

SERVE_ADMIN_BASE_URL=http://xyz.com # URL where the admin endpoint is exposed at.
SERVE_ADMIN_HOST=127.0.0.1 # host on which the admin endpoint listenes on.
SERVE_ADMIN_PORT=4451 # port on which the admin endpoint listens on.

SELF_SERVICE_DEFAULT_BROWSER_RETURN_URL=http://127.0.0.1:4455
SELFSERVICE_ALLOWED_RETURN_URLS_0=http://127.0.0.1:4455
SELFSERVICE_ALLOWED_RETURN_URLS_1=http://127.0.0.1:4433
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

- [Migrations](#migrations)

- [Kratos Integration](#kratos-integration)

- [Code Walk-through](#code-walk-through)
- [Config](#config)
- [Command](#command)
Expand Down Expand Up @@ -74,10 +76,21 @@ for ex: `start` commands run `go run app.go api` if your app.go is somewhere els

Migrations are like **version control for your database**, allowing your team to define and share the application's database schema definition. If you have ever had to tell a teammate to manually add a column to their local database schema after pulling in your changes from source control, you've faced the problem that database migrations solve.

## Kratos Integration
Ory Kratos provides the user identity management service and different flows for user management (signup/sign in, forgot password, reset password, etc.). For more, you can see the official [documentation](https://www.ory.sh/docs/kratos/ory-kratos-intro).

Ory Kratos doesn't provide UI, You have to specify the endpoints for different UI pages inside the configuration and Kratos will use them. There are some other services that you can use for demo UIs. for example `kratos-selfservice-ui-node`.

**Note:** ory Kratos is an optional integration to the boilerplate, if you want to use it you need to follow the below steps.

- Inside the ```.env``` you'll have to set the ```KRATOS_ENABLED``` for enabling the kratos integration.
- According to your config requirements, you will need to change the corresponding files inside the ```/pkg/kratos``` folder.
- Then after that for all the endpoints you want Kratos authentication you'll have to add ```middlewares.Authenticated``` and ```authController.DoKratosAuth```. After that, you can add your own handle and write business logic over there using user details.
- For more details, you can see the [documentation](./pkg/kratos/readme.md) section.

## Execution

1. Run ```docker-compose up``` to spin up database and admire.
1. Run ```docker-compose up``` to spin up the database and admire. In the case of a Kratos Enabled user this command ```docker-compose --profile kratos up```.
2. Open ```localhost:8080```, select **system** to ```PostgreSQL``` and put username and password.
3. Build image using ```docker build -t golang-api .```
4. Run ```docker run golang-api``` to run the container.
Expand Down
36 changes: 36 additions & 0 deletions assets/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@
}
}
},
"/kratos/auth": {
"get": {
"consumes": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"Auth"
],
"summary": "Authenticate user with kratos session id.",
"operationId": "none",
"responses": {
"400": {
"$ref": "#/responses/GenericResFailBadRequest"
},
"500": {
"$ref": "#/responses/GenericResError"
}
}
}
},
"/login": {
"post": {
"consumes": [
Expand Down Expand Up @@ -375,6 +399,10 @@
"type": "string",
"x-go-name": "ID"
},
"kratos_id": {
"type": "string",
"x-go-name": "KratosID"
},
"last_name": {
"type": "string",
"x-go-name": "LastName"
Expand Down Expand Up @@ -424,6 +452,10 @@
"type": "string",
"x-go-name": "ID"
},
"kratos_id": {
"type": "string",
"x-go-name": "KratosID"
},
"last_name": {
"type": "string",
"x-go-name": "LastName"
Expand Down Expand Up @@ -473,6 +505,10 @@
"type": "string",
"x-go-name": "ID"
},
"kratos_id": {
"type": "string",
"x-go-name": "KratosID"
},
"last_name": {
"type": "string",
"x-go-name": "LastName"
Expand Down
26 changes: 26 additions & 0 deletions config/kratos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package config

// DBConfig type of db config object
type KratosConfig struct {
IsEnabled bool `envconfig:"KRATOS_ENABLED"`
BaseUrl string `envconfig:"SERVE_PUBLIC_BASE_URL"`
UIUrl string `envconfig:"SELF_SERVICE_DEFAULT_BROWSER_RETURN_URL"`
AdminUrl string `envconfig:"SERVE_ADMIN_BASE_URL"`
PublicUrl string `envconfig:"SERVE_PUBLIC_BASE_URL"`
CookieExpirationTime string `envconfig:"KRATOS_COOKIE_EXPIRATION_TIME"`
}

type KratosUserDetails struct {
Identity struct {
ID string `json:"id"`
Traits struct {
Name struct {
Last string `json:"last"`
First string `json:"first"`
} `json:"name"`
Email string `json:"email"`
} `json:"traits"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"identity"`
}
1 change: 1 addition & 0 deletions config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type AppConfig struct {
Port string `envconfig:"APP_PORT"`
Secret string `envconfig:"JWT_SECRET"`
DB DBConfig
Kratos KratosConfig
}

// GetConfig Collects all configs
Expand Down
20 changes: 15 additions & 5 deletions constants/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ package constants
// variables
const (
CookieUser = "user"
KratosCookie = "ory_kratos_session"
)

// fiber contexts
const (
ContextUid = "userId"
)

// kratos
const (
KratosID = "kratosId"
KratosUserDetails = "kratosUserDetails"
)
// params
const (
ParamUid = "userId"
Expand All @@ -29,11 +35,15 @@ const (

// Error messages
const (
ErrGetUser = "error while get user"
ErrLoginUser = "error while login user"
ErrInsertUser = "error while creating user, please try after sometime"
ErrHealthCheckDb = "error while checking health of database"
ErrUnauthenticated = "error verifing user identity"
ErrGetUser = "error while get user"
ErrLoginUser = "error while login user"
ErrInsertUser = "error while creating user, please try after sometime"
ErrHealthCheckDb = "error while checking health of database"
ErrUnauthenticated = "error verifing user identity"
ErrKratosAuth = "error while fetching user from kratos"
ErrKratosDataInsertion = "error while inserting user data came from kratos"
ErrKratosIDEmpty = "error no session_id found in kratos cookie"
ErrKratosCookieTime = "error while parsing the expiration time of the cookie"
)

// Events
Expand Down
63 changes: 63 additions & 0 deletions controllers/api/v1/auth_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package v1
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"time"

Expand All @@ -14,13 +15,15 @@ import (
"github.com/Improwised/golang-api/services"
"github.com/Improwised/golang-api/utils"
"github.com/doug-martin/goqu/v9"
"github.com/go-resty/resty/v2"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"gopkg.in/go-playground/validator.v9"
)

type AuthController struct {
userService *services.UserService
userModel *models.UserModel
logger *zap.Logger
config config.AppConfig
}
Expand All @@ -35,6 +38,7 @@ func NewAuthController(goqu *goqu.Database, logger *zap.Logger, config config.Ap

return &AuthController{
userService: userSvc,
userModel: &userModel,
logger: logger,
config: config,
}, nil
Expand Down Expand Up @@ -94,3 +98,62 @@ func (ctrl *AuthController) DoAuth(c *fiber.Ctx) error {

return utils.JSONSuccess(c, http.StatusOK, user)
}

// DoKratosAuth authenticate user with kratos session id
// swagger:route GET /kratos/auth Auth none
//
// Authenticate user with kratos session id.
//
// Consumes:
// - application/json
//
// Schemes: http, https
// Responses:
// 400: GenericResFailBadRequest
// 500: GenericResError
func (ctrl *AuthController) DoKratosAuth(c *fiber.Ctx) error {
kratosID := c.Locals(constants.KratosID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shaktizala What is the purpose of using c.Locals?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used to fetch kratosID into the next handle. It means we can add it to locals from here and fetch it from the next handler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shaktizala Is it middleware? Middleware shouldn't be in controller

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not middleware. used to pass an ID from middleware to next handler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, is DoKratosAuth middleware?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not middleware. It's an endpoint on which kratos will redirect after successful login/registration.


if kratosID.(string) == "" {
return utils.JSONError(c, http.StatusBadRequest, constants.ErrKratosIDEmpty)
}

kratosClient := resty.New().SetBaseURL(ctrl.config.Kratos.BaseUrl+"/sessions").SetHeader("Cookie", fmt.Sprintf("%v=%v", constants.KratosCookie, kratosID)).SetHeader("accept", "application/json")

kratosUser := config.KratosUserDetails{}
res, err := kratosClient.R().SetResult(&kratosUser).Get("/whoami")
if err != nil || res.StatusCode() != http.StatusOK {
return utils.JSONError(c, http.StatusInternalServerError, constants.ErrKratosAuth)
}

userStruct := models.User{}
userStruct.KratosID = kratosUser.Identity.ID
userStruct.FirstName = kratosUser.Identity.Traits.Name.First
userStruct.LastName = kratosUser.Identity.Traits.Name.Last
userStruct.Email = kratosUser.Identity.Traits.Email
userStruct.CreatedAt = kratosUser.Identity.CreatedAt
userStruct.UpdatedAt = kratosUser.Identity.UpdatedAt

user, err := ctrl.userModel.InsertKratosUser(userStruct)
if err != nil {
return utils.JSONError(c, http.StatusInternalServerError, constants.ErrKratosDataInsertion)
}

cookieExpirationTime, err := time.ParseDuration(ctrl.config.Kratos.CookieExpirationTime)
if err != nil {
return utils.JSONError(c, http.StatusInternalServerError, constants.ErrKratosCookieTime)
}

userCookie := &fiber.Cookie{
Name: constants.KratosCookie,
Value: kratosID.(string),
Expires: time.Now().Add(cookieExpirationTime),
}

c.Cookie(userCookie)
c.Redirect(ctrl.config.Kratos.UIUrl)

c.Locals(constants.KratosUserDetails, user)
c.Next()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shaktizala What is purpose of it? L154 will already redirect on UI

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, forgot to remove it, it was there when I was not doing redirection. will update it.

return nil
}
5 changes: 3 additions & 2 deletions database/migrations/000001_create_user_table.up.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
-- +migrate Up
CREATE TABLE IF NOT EXISTS users (
id CHAR (20) PRIMARY KEY,
kratos_id CHAR(50),
first_name VARCHAR (50) NOT NULL,
last_name VARCHAR (50) NOT NULL,
email VARCHAR (50) UNIQUE NOT NULL,
password VARCHAR (100) NOT NULL,
roles TEXT NOT NULL,
password VARCHAR (100),
roles TEXT,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
35 changes: 34 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,53 @@ services:
POSTGRES_USER: golang-api
POSTGRES_PASSWORD: golang-api
POSTGRES_DB: golang-api
POSTGRES_MULTIPLE_DATABASES: '"kratos-db"'
ports:
- 5432:5432
volumes:
- ./pkg/kratos/kratos-pg-init-script:/docker-entrypoint-initdb.d
- pgdata:/var/lib/postgresql/data

adminer:
image: adminer
restart: always
ports:
- 8080:8080
environment:
environment:
- ADMINER_DEFAULT_SERVER=postgresdb
depends_on:
- postgresdb

kratos_migrate:
image: oryd/kratos:v1.0.0
profiles: [kratos]
environment:
- DSN=postgres://golang-api:golang-api@postgresdb:5432/kratos-db?sslmode=disable
- LOG_LEVEL=trace
command: migrate sql -e --yes
restart: on-failure
depends_on:
- postgresdb

kratos:
image: oryd/kratos:v1.0.0
profiles: [kratos]
ports:
- '4433:${SERVE_PUBLIC_PORT:-4433}' # public
- '4434:${SERVE_ADMIN_PORT:-4434}' # admin
environment:
- LOG_LEVEL=trace
env_file:
- .env

command: serve -c /etc/config/kratos/kratos.yml

restart: always
depends_on:
- postgresdb
- kratos_migrate
volumes:
- "./pkg/kratos:/etc/config/kratos"

volumes:
pgdata:
9 changes: 9 additions & 0 deletions middlewares/authenticated.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import (
)

func (m *Middleware) Authenticated(c *fiber.Ctx) error {
if m.config.Kratos.IsEnabled {
sessionID := c.Cookies("ory_kratos_session")
if sessionID == "" {
return utils.JSONFail(c, http.StatusUnauthorized, constants.Unauthenticated)
}
c.Locals(constants.KratosID, sessionID)
return c.Next()
}

token := c.Cookies(constants.CookieUser, "")
if token == "" {
return utils.JSONFail(c, http.StatusUnauthorized, constants.Unauthenticated)
Expand Down
Loading
Loading