Skip to content

Commit

Permalink
Adding multi-provider draft and enabling dev OAuth2 provider (#25)
Browse files Browse the repository at this point in the history
This change adds a draft to a dev OAuth2 provider to help other developers to test features without setting up a Google Auth (and having 2 gmails).

This commit summarize the following changes:
* Added config to enable auth with mock OAuth2 server
* Adding mock OAuth2 server
* Adding dev oauth2 provider
* Adding test to logger
* Fixing minor redirect bug on ExchangeCode
* Updating README
* Enabling token flow
* Adding PR suggestions
* Changing db/up script
* Update README.md (Co-authored-by: Felipe Rodopoulos <felipekss@gmail.com>)
* Improved dependency checking between docker containers
* Changing container start commands to use healthcheck
* Adding PR suggestions to healthcheck
* Updating Readme

Co-authored-by: Daniel Dias <daniel.dias@wildlifestudios.com>
Co-authored-by: Felipe Rodopoulos <felipekss@gmail.com>
  • Loading branch information
3 people committed Sep 1, 2020
1 parent b395900 commit c54320b
Show file tree
Hide file tree
Showing 14 changed files with 435 additions and 56 deletions.
30 changes: 27 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ docker/build:
@docker build -t $(project) .

.PHONY: run
run:
@reflex -c reflex.conf -- sh -c ./bin/Will.IAM start-api
run: build
./bin/Will.IAM start-api --host=localhost -v3

.PHONY: dev-run
dev-run:
@reflex -c reflex.conf -- sh -c make run

.PHONY: test
test: db/setup-test test/unit test/integration db/stop-test
Expand All @@ -53,20 +57,40 @@ endif
download-mod:
@go mod download

# stop all containers, removing container data
.PHONY: compose-down
compose-down:
@docker-compose down

# stop all containers, preserving container data
.PHONY: compose-stop
compose-stop:
@docker-compose stop

# start all containers
.PHONY: compose-up
compose-up:
@docker-compose up william

# start only the dependency containers
.PHONY: dependencies/up
dependencies/up:
@mkdir -p docker_data && docker-compose up -d postgres oauth2-server
@until docker inspect --format "{{json .State.Health.Status }}" Will.IAM_postgres_1 | grep -q "healthy"; do echo 'Waiting Postgres...' && sleep 1; done
@until docker inspect --format "{{json .State.Health.Status }}" Will.IAM_oauth2_server_1 | grep -q "healthy"; do echo 'Waiting OAuth2 server...' && sleep 1; done
@sleep 2

.PHONY: db/setup
db/setup: db/up db/create-user db/create db/migrate

.PHONY: db/setup-test
db/setup-test: db/up db/create-user db/create-test db/migrate-test

# start only the database container
.PHONY: db/up
db/up:
@mkdir -p docker_data && docker-compose up -d postgres
@until docker exec $(pg_docker_image) pg_isready; do echo 'Waiting Postgres...' && sleep 1; done
@until docker inspect --format "{{json .State.Health.Status }}" Will.IAM_postgres_1 | grep -q "healthy"; do echo 'Waiting Postgres...' && sleep 1; done
@sleep 2

.PHONY: db/create-user
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ A nice-to-have feature would be to declare permission dependencies. It should be
One way to do this is to have clients declare them over a Will.IAM endpoint and use this custom entity,
PermissionDependency, when creating / deleting user|role permissions.

## Developing Will.IAM

To start to develop in Will.IAM codebase be sure that you have `go 1.13` and `Docker` installed.

After that, on the root folder, do the following steps:
1. Execute `make db/setup` to setup the service database;
2. Start the auxiliary services and Will.IAM using `make compose-up`.

If you want to run Will.IAM locally (for example to use code reload with reflex or use a debugger), you can use `make dependencies/up` to start the dependency containers and later run `make dev-run` to start Will.IAM in development mode.

## TODO:

### major
Expand Down
15 changes: 5 additions & 10 deletions api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (a *App) configureApp() error {
return err
}

a.configureGoogleOAuth2Provider()
a.configureOAuth2Provider()
a.configureServer()

return nil
Expand Down Expand Up @@ -107,16 +107,11 @@ func (a *App) configureJaeger() error {
return err
}

func (a *App) configureGoogleOAuth2Provider() {
func (a *App) configureOAuth2Provider() {
repo := repositories.New(a.storage)
google := oauth2.NewGoogle(oauth2.GoogleConfig{
ClientID: a.config.GetString("oauth2.google.clientId"),
ClientSecret: a.config.GetString("oauth2.google.clientSecret"),
RedirectURL: a.config.GetString("oauth2.google.redirectUrl"),
CheckHostedDomain: a.config.GetBool("oauth2.google.checkHostedDomain"),
HostedDomains: a.config.GetStringSlice("oauth2.google.hostedDomains"),
}, repo)
a.oauth2Provider = google
provider := oauth2.GetOAuthProvider(a.config, repo)

a.SetOAuth2Provider(provider)
}

// SetOAuth2Provider sets a provider in App
Expand Down
2 changes: 2 additions & 0 deletions api/authentication_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func authenticationValidHandler(
authResult, err := sasUC.WithContext(r.Context()).
AuthenticateAccessToken(qs["accessToken"][0])
referer := qs["referer"][0]

if err != nil {
l.WithError(err).Error("authenticationValidHandler AuthenticateAccessToken failed")
v := url.Values{}
Expand All @@ -120,6 +121,7 @@ func authenticationValidHandler(
if strings.Contains(referer, "?") {
sep = "&"
}

http.Redirect(
w, r, fmt.Sprintf("%s%s%s", referer, sep, v.Encode()),
http.StatusSeeOther,
Expand Down
5 changes: 5 additions & 0 deletions config/local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ jaeger:
samplingProbability: 1
serviceName: Will.IAM
oauth2:
provider: dev
dev:
authorizationUrl: http://localhost:9000/authorize
tokenUrl: http://localhost:9000/token
redirectUrl: http://localhost:4040/sso/auth/done
google:
clientId: dummy
clientSecret: dummy
Expand Down
36 changes: 35 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
version: '2'
version: '2.1'
services:
william:
image: golang:1.13-alpine
ports:
- 4040:4040
working_dir: /Will.IAM
volumes:
- ./:/Will.IAM
depends_on:
oauth2-server:
condition: service_healthy
postgres:
condition: service_healthy
environment:
- WILLIAM_EXTENSIONS_PG_HOST=postgres
- WILLIAM_EXTENSIONS_PG_PORT=5432
container_name: Will.IAM_app_1
command: "sh -c 'apk add make && go mod download && make run'"
oauth2-server:
image: node:14.8
ports:
- 9000:9000
container_name: Will.IAM_oauth2_server_1
command: sh -c "npm install -g oauth2-mock-server && oauth2-mock-server -p 9000"
healthcheck:
test: ["CMD", "curl", "http://localhost:9000/.well-known/openid-configuration"]
interval: 1s
timeout: 10s
retries: 5
postgres:
image: postgres:9.6
ports:
Expand All @@ -13,3 +41,9 @@ services:
- "max_connections=9999"
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 1s
timeout: 10s
retries: 5

136 changes: 136 additions & 0 deletions oauth2/dev.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package oauth2

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"

"github.com/topfreegames/Will.IAM/models"
"github.com/topfreegames/Will.IAM/repositories"
extensionsHttp "github.com/topfreegames/extensions/http"
)

// DevOAuth2Provider is a Provider used in development environment
type DevOAuth2Provider struct {
config DevOAuth2ProviderConfig
repo *repositories.All
}

// DevOAuth2ProviderConfig are the basic required informations to use
// our OAuth2 dev server as oauth2 provider
type DevOAuth2ProviderConfig struct {
RedirectURL string
AuthorizationURL string
TokenURL string
}

// NewDevOAuth2Provider ctor
func NewDevOAuth2Provider(config DevOAuth2ProviderConfig, repo *repositories.All) *DevOAuth2Provider {
return &DevOAuth2Provider{
config: config,
repo: repo,
}
}

// BuildAuthURL creates the url used to authorize an user against OAuth2 dev server
func (p *DevOAuth2Provider) BuildAuthURL(state string) string {
return fmt.Sprintf("%s?response_type=code&redirect_uri=%s&state=%s", p.config.AuthorizationURL, p.config.RedirectURL, state)
}

// ExchangeCode validates an auth code against a OAuth2 server
func (p *DevOAuth2Provider) ExchangeCode(code string) (*models.AuthResult, error) {
token, err := p.getToken(code)

if err != nil {
return nil, err
}

// with the token in hands we could go to the service behind the OAuth2 server
// and retrieve some data, like user email and photo

token.Email = "any@example.org"
token.Expiry = time.Now().UTC().Add(14 * 24 * 3600 * time.Second)

if err := p.repo.Tokens.Save(token); err != nil {
return nil, err
}
return &models.AuthResult{
AccessToken: token.AccessToken,
Email: token.Email,
Picture: "http://lorempixel.com/400/200/cats",
}, nil
}

// Authenticate verifies if an accessToken is valid and maybe refresh it
func (p *DevOAuth2Provider) Authenticate(accessToken string) (*models.AuthResult, error) {
return &models.AuthResult{
AccessToken: accessToken,
Email: "any",
}, nil
}

// WithContext does nothing
func (p *DevOAuth2Provider) WithContext(ctx context.Context) Provider {
return p
}

// OAuthToken represents a token received from an OAuth2 server
type OAuthToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn float64 `json:"expires_in"`
}

func (p *DevOAuth2Provider) getToken(code string) (*models.Token, error) {
v := url.Values{}
v.Add("code", code)
v.Add("redirect_uri", p.config.RedirectURL)
v.Add("grant_type", "authorization_code")

req, err := http.NewRequest("POST", p.config.TokenURL, strings.NewReader(v.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

if err != nil {
return nil, err
}

client := extensionsHttp.New()

res, err := client.Do(req)
if err != nil {
return nil, err
}

defer res.Body.Close()

body, _ := ioutil.ReadAll(res.Body)

oauthToken := &OAuthToken{}
err = json.Unmarshal(body, oauthToken)
if err != nil {
return nil, err
}

accessToken := oauthToken.AccessToken

// TODO: Will.IAM is not able to support JWT tokens due a restriction of 300 characters
// in accessToken field when saving a Token on database
if len(accessToken) > 300 {
accessToken = accessToken[0:300]
}

return &models.Token{
AccessToken: accessToken,
RefreshToken: oauthToken.RefreshToken,
TokenType: oauthToken.TokenType,
Expiry: time.Now().UTC().Add(
time.Second * time.Duration(oauthToken.ExpiresIn),
),
}, nil
}
48 changes: 48 additions & 0 deletions oauth2/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package oauth2

import (
"context"

"github.com/topfreegames/Will.IAM/models"
"github.com/topfreegames/Will.IAM/repositories"
)

// ProviderBlankMock is a Provider mock will all dummy implementations
type ProviderBlankMock struct {
Email string
repo *repositories.All
}

// NewProviderBlankMock ctor
func NewProviderBlankMock() *ProviderBlankMock {
return &ProviderBlankMock{}
}

// BuildAuthURL dummy
func (p *ProviderBlankMock) BuildAuthURL(any string) string {
return "any"
}

// ExchangeCode dummy
func (p *ProviderBlankMock) ExchangeCode(any string) (*models.AuthResult, error) {
return &models.AuthResult{
AccessToken: "any",
Email: "any",
}, nil
}

// Authenticate dummy
func (p *ProviderBlankMock) Authenticate(accessToken string) (*models.AuthResult, error) {
tokensRepo := p.repo.Tokens
token, _ := tokensRepo.Get(accessToken)

return &models.AuthResult{
AccessToken: token.AccessToken,
Email: token.Email,
}, nil
}

// WithContext does nothing
func (p *ProviderBlankMock) WithContext(ctx context.Context) Provider {
return p
}
Loading

0 comments on commit c54320b

Please sign in to comment.