Step 4. Authentication and sessions

In most cases, access to an application’s (service’s) resources is granted only after users authenticate themselves in the system. Authentication is the process of checking the validity of the identifier provided by a user. A successful authentication (besides establishing a trusted relationship and generating a session key) is usually also followed up by user authorization. This second step grants the user access rights to an approved set of resources, deemed necessary for the user to perform his/her tasks.

Just like in the previous step, we’ll be placing the files of this step in the operation/version1 folder.

Let’s start by defining a data model for storing user information within a session. Create a new file named SessionUserV1.py with the following code:

src/operations/version1/SessionUserV1.ts

export class SessionUserV1 {
    /* Identification */
    public id: string;
    public login: string;
    public name: string;
    public create_time: Date;

    /* User information */
    public time_zone: string;
    public language: string;
    public theme: string;

    /* Security info **/
    public roles: string[];
    public change_pwd_time: Date;
    public sites: [{ id: string, name: string }];
    public settings: any;

    /* Custom fields */
    public custom_hdr: any;
    public custom_dat: any;
}



/operations/version1/SessionUserV1.go

package operations1

import (
	"time"
)

type SessionUserV1 struct {
	/* Identification */
	Id         string    `json:"id"`
	Login      string    `json:"login"`
	Name       string    `json:"name"`
	CreateTime time.Time `json:"create_time"`

	/* User information */
	TimeZone string `json:"tim_zone"`
	Language string `json:"language"`
	Theme    string `json:"theme"`

	/* Security info **/
	Roles         []string    `json:"roles"`
	ChangePwdTime time.Time   `json:"change_pwd_time"`
	Sites         Site        `json:"sites"`
	Settings      interface{} `json:"settings"`

	/* Custom fields */
	CustomHdr interface{} `json:"custom_hdr"`
	CustomDat interface{} `json:"custom_dat"`
}

type Site struct {
	Id   string `json:"id"`
	Name string `json:"name"`
}

Not available

pip_facades_sample_python/operations/version1/SessionUserV1.py

# -*- coding: utf-8 -*-
import datetime
from typing import List, Any


class SessionUserV1:
    def __init__(self, id: str = None, login: str = None, name: str = None, create_time: str = None,
                 time_zone: str = None, language: str = None,
                 theme: str = None, roles: List[str] = None, change_pwd_time: datetime.datetime = None,
                 sites: List[dict] = None, settings: Any = None, custom_hdr: Any = None,
                 custom_dat: Any = None):

        # Identification
        self.id = id
        self.login = login
        self.name = name
        self.create_time = create_time

        # User information
        self.time_zone = time_zone
        self.language = language
        self.theme = theme

        # Security info
        self.roles = roles
        self.change_pwd_time = change_pwd_time
        self.sites = sites
        self.settings = settings

        # Custom fields
        self.custom_hdr = custom_hdr
        self.custom_dat = custom_dat

Not available

This data model will contain all necessary information about the user: the session’s ID, login, username, list of rolls, etc.

We’ll be defining our operations for managing sessions and authenticating users in a file named SessionOperationsV1.py. A listing of this file’s code is presented below:

src/operations/version1/SessionOperationsV1.ts

import { ConfigParams } from 'pip-services4-components-node';
import { FilterParams } from 'pip-services4-data-node';
import { IReferences } from 'pip-services4-components-node';
import { Descriptor } from 'pip-services4-components-node'; 
import { BadRequestException } from 'pip-services4-commons-node';
import { UnauthorizedException } from 'pip-services4-commons-node';
import { HttpRequestDetector } from 'pip-services4-http-node';
import { RestOperations } from 'pip-services4-http-node';

import { ISettingsClientV1 } from '../../clients/version1/ISettingsClientV1';
import { IAccountsClientV1 } from '../../clients/version1/IAccountsClientV1';
import { AccountV1 } from '../../clients/version1/AccountV1';
import { IPasswordsClientV1 } from '../../clients/version1/IPasswordsClientV1';
import { UserPasswordInfoV1 } from '../../clients/version1/UserPasswordInfoV1';
import { IRolesClientV1 } from '../../clients/version1/IRolesClientV1';
import { ISessionsClientV1 } from '../../clients/version1/ISessionsClientV1';
import { SessionV1 } from '../../clients/version1/SessionV1';
import { IEmailSettingsClientV1 } from '../../clients/version1/IEmailSettingsClientV1';
import { EmailSettingsV1 } from '../../clients/version1/EmailSettingsV1';
import { ISitesClientV1 } from '../../clients/version1/ISitesClientV1';
import { IInvitationsClientV1 } from '../../clients/version1/IInvitationsClientV1';
import { SiteV1 } from '../../clients/version1/SiteV1';

import { SessionUserV1 } from './SessionUserV1';

export class SessionsOperationsV1  extends RestOperations {
    private static _defaultConfig1 = ConfigParams.fromTuples(
        'options.cookie_enabled', true,
        'options.cookie', 'x-session-id',
        'options.max_cookie_age', 365 * 24 * 60 * 60 * 1000
    );

    private _cookie: string = 'x-session-id';
    private _cookieEnabled: boolean = true;
    private _maxCookieAge: number = 365 * 24 * 60 * 60 * 1000;

    private _settingsClient: ISettingsClientV1;
    private _accountsClient: IAccountsClientV1;
    private _sessionsClient: ISessionsClientV1;
    private _passwordsClient: IPasswordsClientV1;
    private _rolesClient: IRolesClientV1;
    private _emailSettingsClient: IEmailSettingsClientV1;
    private _sitesClient: ISitesClientV1;
    private _invitationsClient: IInvitationsClientV1;

    public constructor() {
        super();

        this._dependencyResolver.put('settings', new Descriptor('pip-services-settings', 'client', '*', '*', '1.0'));
        this._dependencyResolver.put('accounts', new Descriptor('pip-services-accounts', 'client', '*', '*', '1.0'));
        this._dependencyResolver.put('passwords', new Descriptor('pip-services-passwords', 'client', '*', '*', '1.0'));
        this._dependencyResolver.put('roles', new Descriptor('pip-services-roles', 'client', '*', '*', '1.0'));
        this._dependencyResolver.put('emailsettings', new Descriptor('pip-services-emailsettings', 'client', '*', '*', '1.0'));
        this._dependencyResolver.put('sessions', new Descriptor('pip-services-sessions', 'client', '*', '*', '1.0'));
        this._dependencyResolver.put('sites', new Descriptor('pip-services-sites', 'client', '*', '*', '1.0'));
        this._dependencyResolver.put('invitations', new Descriptor('pip-services-invitations', 'client', '*', '*', '1.0'));
    }

    public configure(config: ConfigParams): void {
        config = config.setDefaults(SessionsOperationsV1._defaultConfig1);
        this._dependencyResolver.configure(config);

        this._cookieEnabled = config.getAsBooleanWithDefault('options.cookie_enabled', this._cookieEnabled);
        this._cookie = config.getAsStringWithDefault('options.cookie', this._cookie);
        this._maxCookieAge = config.getAsLongWithDefault('options.max_cookie_age', this._maxCookieAge);
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);

        this._settingsClient = this._dependencyResolver.getOneRequired<ISettingsClientV1>('settings');
        this._sessionsClient = this._dependencyResolver.getOneRequired<ISessionsClientV1>('sessions');
        this._accountsClient = this._dependencyResolver.getOneRequired<IAccountsClientV1>('accounts');
        this._passwordsClient = this._dependencyResolver.getOneRequired<IPasswordsClientV1>('passwords');
        this._rolesClient = this._dependencyResolver.getOneRequired<IRolesClientV1>('roles');
        this._emailSettingsClient = this._dependencyResolver.getOneOptional<IEmailSettingsClientV1>('emailsettings');
        this._sitesClient = this._dependencyResolver.getOneRequired<ISitesClientV1>('sites');
        this._invitationsClient = this._dependencyResolver.getOneRequired<IInvitationsClientV1>('invitations');
    }
    
    public async loadSession(req: any, res: any, next: () => void): Promise<void> {
        // parse headers first, and if nothing in headers get cookie
        let sessionId = req.headers['x-session-id'];
        
        if (sessionId) {
            let session = await this._sessionsClient.getSessionById('facade', sessionId);

            if (session == null) {
                let err = new UnauthorizedException(
                    'facade',
                    'SESSION_NOT_FOUND',
                    'Session invalid or already expired.'
                ).withDetails('session_id', sessionId).withStatus(440);

                this.sendError(req, res, err);
            } else {
                // Associate session user with the request
                req.user_id = session.user_id;
                req.user_name = session.user_name;
                req.user = session.user;
                req.session_id = session.id;
                next();
            }
        } else {
            next();
        }
    }

    public async openSession(req: any, res: any, account: AccountV1, roles: string[]): Promise<void> {
        let session: SessionV1;
        let sites: SiteV1[];
        let passwordInfo: UserPasswordInfoV1;
        let settings: ConfigParams;

        try {
            let siteRoles = roles.filter(r => r.indexOf(':') > 0);

            let siteIds = siteRoles.map((r) => {
                let pos = r.indexOf(':');
                return pos >= 0 ? r.substring(0, pos) : r;
            });

            if (siteIds.length > 0) {
                let filter = FilterParams.fromTuples('ids', siteIds);
                let page = await this._sitesClient.getSites(null, filter, null);
                sites = page != null ? page.data : [];
            } else {
                sites = [];
            }

            passwordInfo = await this._passwordsClient.getPasswordInfo(null, account.id);

            settings = await this._settingsClient.getSectionById(null, account.id);

            // Open a new user session
            let user = <SessionUserV1>{
                id: account.id,
                name: account.name,
                login: account.login,
                create_time: account.create_time,
                time_zone: account.time_zone,
                language: account.language,
                theme: account.theme,
                roles: roles,
                sites: sites.map(s => { return { id: s.id, name: s.name } }),
                settings: settings,
                change_pwd_time: passwordInfo != null ? passwordInfo.change_time : null,
                custom_hdr: account.custom_hdr,
                custom_dat: account.custom_dat
            };

            let address = HttpRequestDetector.detectAddress(req);
            let client = HttpRequestDetector.detectBrowser(req);
            let platform = HttpRequestDetector.detectPlatform(req);

            session = await this._sessionsClient.openSession(
                null, account.id, account.name,
                address, client, user, null
            );

            this.sendResult(req, res, session);
        } catch (err) {
            this.sendError(req, res, err);
        }
    }

    public async signup(req: any, res: any): Promise<void> {
        let signupData = req.body;
        let account: AccountV1 = null;
        let invited: boolean = false;
        let roles: string[] = [];

        try {
            // Validate password first
            // Todo: complete implementation after validate password is added

            // Create account
            let newAccount = <AccountV1>{
                name: signupData.name,
                login: signupData.login || signupData.email, // Use email by default
                language: signupData.language,
                theme: signupData.theme,
                time_zone: signupData.time_zone
            };

            account = await this._accountsClient.createAccount(
                null, newAccount
            )

            // Create password for the account
            let password = signupData.password;

            this._passwordsClient.setPassword(
                null, account.id, password
            );

            // Activate all pending invitations
            let email = signupData.email;

            let invitations = await this._invitationsClient.activateInvitations(
                null, email, account.id);

            if (invitations) {
                // Calculate user roles from activated invitations
                for (let invitation of invitations) {
                    // Was user invited with the same email?
                    invited = invited || email == invitation.invitee_email;

                    if (invitation.site_id) {
                        invitation.role = invitation.role || 'user';
                        let role = invitation.site_id + ':' + invitation.role;
                        roles.push(role);
                    }
                }
            }

            // Create email settings for the account
            let newEmailSettings = <EmailSettingsV1>{
                id: account.id,
                name: account.name,
                email: email,
                language: account.language
            };

            if (this._emailSettingsClient != null) {
                if (invited) {
                    this._emailSettingsClient.setVerifiedSettings(
                        null, newEmailSettings
                    );
                } else {
                    this._emailSettingsClient.setSettings(
                        null, newEmailSettings
                    );
                }
            }

            await this.openSession(req, res, account, roles);
        }
        catch (err) {
            this.sendError(req, res, err);
        }
    }

    public async signupValidate(req: any, res: any): Promise<void> {
        let login = req.param('login');

        if (login) {
            let account = await this._accountsClient.getAccountByIdOrLogin(null, login);
            if (account != null) {
                let err = new BadRequestException(
                    null, 'LOGIN_ALREADY_USED',
                    'Login ' + login + ' already being used'
                ).withDetails('login', login);

                this.sendError(req, res, err);
            } else this.sendEmptyResult(req, res);
        }
        else {
            this.sendEmptyResult(req, req);
        }
    }

    public async signin(req: any, res: any): Promise<void> {
        let login = req.param('login');
        let password = req.param('password');

        let account: AccountV1;
        let roles: string[] = [];

        try {
            // Find user account
            account = await this._accountsClient.getAccountByIdOrLogin(null, login);

            if (account == null) {
                throw new BadRequestException(
                    null,
                    'WRONG_LOGIN',
                    'Account ' + login + ' was not found'
                ).withDetails('login', login);
            }

            // Authenticate user
            let result = await this._passwordsClient.authenticate(null, account.id, password);

            // wrong password error is UNKNOWN when use http client
            if (result == false) {
                throw new BadRequestException(
                    null,
                    'WRONG_PASSWORD',
                    'Wrong password for account ' + login
                ).withDetails('login', login);
            }

            // Retrieve user roles
            if (this._rolesClient) {
                roles = await this._rolesClient.getRolesById(null, account.id);
            }

            await this.openSession(req, res, account, roles);
        } catch (err) {
            this.sendError(req, res, err);
        }
    }

    public async signout(req: any, res: any): Promise<void> {
        if (req.session_id) {
            try {
                await this._sessionsClient.closeSession(null, req.session_id);
                this.sendEmptyResult(req, res)
            } catch (err) {
                this.sendError(req, res, err);
            }
        } else {
            this.sendEmptyResult(req, res)
        }
    }

    public async getSessions(req: any, res: any): Promise<void> {
        let filter = this.getFilterParams(req);
        let paging = this.getPagingParams(req);

        let sessions = await this._sessionsClient.getSessions(
            null, filter, paging
        );

        this.sendResult(req, res, sessions);
    }

    public async restoreSession(req: any, res: any): Promise<void> {
        let sessionId = req.param('session_id');

        let session = await this._sessionsClient.getSessionById(null, sessionId);

        // If session closed then return null
        if (session && !session.active)
            session = null;

        if (session)
            this.sendResult(req, res, session);
        else
            this.sendEmptyResult(req, res);
    }

    public async getUserSessions(req: any, res: any): Promise<void> {
        let filter = this.getFilterParams(req);
        let paging = this.getPagingParams(req);
        let userId = req.params.user_id || req.params.account_id;
        filter.setAsObject('user_id', userId);

        let result = await this._sessionsClient.getSessions(
            null, filter, paging
        );

        this.sendResult(req, res, result);
    }

    public async getCurrentSession(req: any, res: any): Promise<void> {
        // parse headers first, and if nothing in headers get cookie
        let sessionId = req.headers['x-session-id']; 

        let result = await this._sessionsClient.getSessionById(null, sessionId);

        this.sendResult(req, res, result);
    }

    public async closeSession(req: any, res: any): Promise<void> {
        let sessionId = req.params.session_id || req.param('session_id');

        let result = await this._sessionsClient.closeSession(null, sessionId);

        this.sendResult(req, res, result);
    }

}


/operations/version1/SessionOperationsV1.go

package operations1

import (
	"context"
	"encoding/json"
	"net/http"
	"sync"
	"time"

	accclients1 "github.com/pip-services-users/pip-clients-accounts-go/version1"
	passclients1 "github.com/pip-services-users/pip-clients-passwords-go/version1"
	roleclients1 "github.com/pip-services-users/pip-clients-roles-go/version1"
	sessclients1 "github.com/pip-services-users/pip-clients-sessions-go/version1"

	cdata "github.com/pip-services4/pip-services4-go/pip-services4-commons-go/data"
	cerr "github.com/pip-services4/pip-services4-go/pip-services4-commons-go/errors"
	cconf "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
	cref "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
	httpcontroller "github.com/pip-services4/pip-services4-go/pip-services4-http-go/controllers"
)

type SessionsOperationsV1 struct {
	*httpcontroller.RestOperations
	defaultConfig *cconf.ConfigParams

	cookie        string
	cookieEnabled bool
	maxCookieAge  int64

	accountsClient  accclients1.IAccountsClientV1
	sessionsClient  sessclients1.ISessionsClientV1
	passwordsClient passclients1.IPasswordsClientV1
	rolesClient     roleclients1.IRolesClientV1
}

func NewSessionsOperationsV1(ctx context.Context) *SessionsOperationsV1 {
	c := SessionsOperationsV1{
		RestOperations: httpcontroller.NewRestOperations(),
	}

	c.defaultConfig = cconf.NewConfigParamsFromTuples(
		"options.cookie_enabled", true,
		"options.cookie", "x-session-id",
		"options.max_cookie_age", 365*24*60*60*1000,
	)
	c.cookie = "x-session-id"
	c.cookieEnabled = true
	c.maxCookieAge = 365 * 24 * 60 * 60 * 1000

	c.DependencyResolver.Put(ctx, "accounts", cref.NewDescriptor("pip-services-accounts", "client", "*", "*", "1.0"))
	c.DependencyResolver.Put(ctx, "passwords", cref.NewDescriptor("pip-services-passwords", "client", "*", "*", "1.0"))
	c.DependencyResolver.Put(ctx, "roles", cref.NewDescriptor("pip-services-roles", "client", "*", "*", "1.0"))
	c.DependencyResolver.Put(ctx, "sessions", cref.NewDescriptor("pip-services-sessions", "client", "*", "*", "1.0"))
	return &c
}

func (c *SessionsOperationsV1) Configure(ctx context.Context, config *cconf.ConfigParams) {
	config = config.SetDefaults(c.defaultConfig)
	c.DependencyResolver.Configure(ctx, config)

	c.cookieEnabled = config.GetAsBooleanWithDefault("options.cookie_enabled", c.cookieEnabled)
	c.cookie = config.GetAsStringWithDefault("options.cookie", c.cookie)
	c.maxCookieAge = config.GetAsLongWithDefault("options.max_cookie_age", c.maxCookieAge)
}

func (c *SessionsOperationsV1) SetReferences(ctx context.Context, references cref.IReferences) {
	c.RestOperations.SetReferences(ctx, references)

	dependency, _ := c.DependencyResolver.GetOneRequired("sessions")
	sesionsClient, ok1 := dependency.(sessclients1.ISessionsClientV1)
	if !ok1 {
		panic("SessionOperationsV1: Cant't resolv dependency 'client' to ISessionsClientV1")
	}
	c.sessionsClient = sesionsClient

	dependency, _ = c.DependencyResolver.GetOneRequired("accounts")
	acountClient, ok2 := dependency.(accclients1.IAccountsClientV1)
	if !ok2 {
		panic("SessionOperationsV1: Cant't resolv dependency 'client' to IAccountsClientV1")
	}
	c.accountsClient = acountClient

	dependency, _ = c.DependencyResolver.GetOneRequired("passwords")
	passClient, ok3 := dependency.(passclients1.IPasswordsClientV1)
	if !ok3 {
		panic("SessionOperationsV1: Cant't resolv dependency 'client' to IPasswordsClientV1")
	}
	c.passwordsClient = passClient

	dependency, _ = c.DependencyResolver.GetOneRequired("roles")
	rolesClient, ok4 := dependency.(roleclients1.IRolesClientV1)
	if !ok4 {
		panic("SessionOperationsV1: Cant't resolv dependency 'client' to IRolesClientV1")
	}
	c.rolesClient = rolesClient
}

func (c *SessionsOperationsV1) LoadSession(res http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
	sessionId := req.Header.Get("x-session-id")

	if sessionId != "" {
		session, err := c.sessionsClient.GetSessionById("facade", sessionId)
		if session == nil && err == nil {
			err = cerr.NewUnauthorizedError(
				"facade",
				"SESSION_NOT_FOUND",
				"Session invalid or already expired.",
			).WithDetails("session_id", sessionId).WithStatus(440)
		}

		if err == nil {
			// Associate session user with the request
			buf, _ := json.Marshal(session.User)
			user := make(map[string]interface{}, 0)
			json.Unmarshal(buf, &user)
			req = req.WithContext(context.WithValue(req.Context(), "user_id", session.UserId))
			req = req.WithContext(context.WithValue(req.Context(), "user_name", session.UserName))
			req = req.WithContext(context.WithValue(req.Context(), "user", *cdata.NewAnyValueMapFromValue(user)))
			req = req.WithContext(context.WithValue(req.Context(), "session_id", session.Id))
			next.ServeHTTP(res, req)
		} else {
			c.SendError(res, req, err)
		}

	} else {
		next.ServeHTTP(res, req)
	}
}

func (c *SessionsOperationsV1) OpenSession(res http.ResponseWriter, req *http.Request, account *accclients1.AccountV1, roles []string) {
	var session *sessclients1.SessionV1
	var settings cconf.ConfigParams

	wg := sync.WaitGroup{}
	wg.Add(1)

	go func() {

		defer wg.Done()
		passwordInfo, err := c.passwordsClient.GetPasswordInfo("", account.Id)
		if err != nil {
			c.SendError(res, req, err)
			return
		}

		var changePwdTime time.Time
		if passwordInfo != nil {
			changePwdTime = passwordInfo.ChangeTime
		}

		var user = SessionUserV1{
			Id:            account.Id,
			Name:          account.Name,
			Login:         account.Login,
			CreateTime:    account.CreateTime,
			TimeZone:      account.TimeZone,
			Language:      account.Language,
			Theme:         account.Theme,
			Roles:         roles,
			Settings:      settings,
			ChangePwdTime: changePwdTime,
			CustomHdr:     account.CustomHdr,
			CustomDat:     account.CustomDat,
		}

		address := httpcontroller.HttpRequestDetector.DetectAddress(req)
		client := httpcontroller.HttpRequestDetector.DetectBrowser(req)

		session, err = c.sessionsClient.OpenSession("", account.Id, account.Name, address, client, user, nil)
		if err != nil {
			c.SendError(res, req, err)
			return
		}
		//res.json(session)
		c.SendResult(res, req, session, nil)
	}()

	wg.Wait()

}

func (c *SessionsOperationsV1) Signup(res http.ResponseWriter, req *http.Request) {

	signupData := make(map[string]interface{})
	c.DecodeBody(req, &signupData)
	r, ok := signupData["roles"].([]string)
	roles := make([]string, 0)
	if ok {
		roles = append(roles, r...)
	}
	wg := sync.WaitGroup{}
	wg.Add(1)

	go func() {
		defer wg.Done()
		// Use email by default
		login, ok := signupData["login"].(string)
		if !ok {
			login, _ = signupData["email"].(string)
		}
		// Create account
		newAccount := accclients1.AccountV1{}
		newAccount.Name, _ = signupData["name"].(string)
		newAccount.Login = login
		newAccount.Language, _ = signupData["language"].(string)
		newAccount.Theme, _ = signupData["theme"].(string)
		newAccount.TimeZone, _ = signupData["time_zone"].(string)

		account, err := c.accountsClient.CreateAccount("", &newAccount)
		if err != nil {
			c.SendError(res, req, err)
			return
		}

		// Create password for the account
		password, _ := signupData["password"].(string)

		err = c.passwordsClient.SetPassword(
			"", account.Id, password)
		if err != nil {
			c.SendError(res, req, err)
			return
		}
		// Create roles for the account
		if len(roles) > 0 {
			c.rolesClient.GrantRoles(
				"", account.Id, roles)
		}
		c.OpenSession(res, req, account, roles)
	}()
	wg.Wait()
}

func (c *SessionsOperationsV1) Signin(res http.ResponseWriter, req *http.Request) {

	login := c.GetParam(req, "login")
	password := c.GetParam(req, "password")

	if login == "" && password == "" {
		params := make(map[string]string, 0)
		c.DecodeBody(req, &params)
		login = params["login"]
		password = params["password"]
	}

	roles := make([]string, 0)

	wg := sync.WaitGroup{}
	wg.Add(1)

	go func() {
		defer wg.Done()
		// Find user account

		account, err := c.accountsClient.GetAccountByIdOrLogin("", login)
		if err != nil {
			c.SendError(res, req, err)
			return
		}
		if err == nil && account == nil {
			err = cerr.NewBadRequestError(
				"",
				"WRONG_LOGIN",
				"Account "+login+" was not found",
			).WithDetails("login", login)
			if err != nil {
				c.SendError(res, req, err)
				return
			}
		}

		// Authenticate user
		result, err := c.passwordsClient.Authenticate("", account.Id, password)

		// wrong password error is UNKNOWN when use http client
		if (err == nil && result == false) || (err != nil && err.(*cerr.ApplicationError).Cause == "Invalid password") {
			err = cerr.NewBadRequestError(
				"",
				"WRONG_PASSWORD",
				"Wrong password for account "+login,
			).WithDetails("login", login)
			if err != nil {
				c.SendError(res, req, err)
				return
			}
		}

		// Retrieve user roles
		if c.rolesClient != nil {
			roles, err = c.rolesClient.GetRolesById("", account.Id)
			if err != nil {
				c.SendError(res, req, err)
				return
			}

		} else {
			roles = make([]string, 0)
		}

		c.OpenSession(res, req, account, roles)
	}()
	wg.Wait()

}

func (c *SessionsOperationsV1) Signout(res http.ResponseWriter, req *http.Request) {

	sessionId, ok := req.Context().Value("session_id").(string)
	if ok {
		_, err := c.sessionsClient.CloseSession("", sessionId)

		if err != nil {
			c.SendError(res, req, err)
		} else {
			// res.json(204);
			c.SendEmptyResult(res, req, nil)
		}

	} else {
		//res.json(204);
		c.SendEmptyResult(res, req, nil)
	}
}

Not available

pip_facades_sample_python/operations/version1/SessionOperationsV1.py

# -*- coding: utf-8 -*-
from typing import List

import bottle
from pip_services4_components.config import ConfigParams
from pip_services4_commons.convert import JsonConverter, TypeCode
from pip_services4_data.query import FilterParams
from pip_services4_commons.errors import UnauthorizedException, BadRequestException
from pip_services4_components.refer import IReferences, Descriptor
from pip_services4_http.controller import RestOperations
from pip_services4_http.controller.HttpRequestDetector import HttpRequestDetector

from pip_facades_sample_python.clients.version1.AccountV1 import AccountV1
from pip_facades_sample_python.clients.version1.EmailSettingsV1 import EmailSettingsV1
from pip_facades_sample_python.clients.version1.IAccountsClientV1 import IAccountsClientV1
from pip_facades_sample_python.clients.version1.IEmailSettingsClientV1 import IEmailSettingsClientV1
from pip_facades_sample_python.clients.version1.IInvitationsClientV1 import IInvitationsClientV1
from pip_facades_sample_python.clients.version1.IPasswordsClientV1 import IPasswordsClientV1
from pip_facades_sample_python.clients.version1.IRolesClientV1 import IRolesClientV1
from pip_facades_sample_python.clients.version1.ISessionsClientV1 import ISessionsClientV1
from pip_facades_sample_python.clients.version1.ISettingsClientV1 import ISettingsClientV1
from pip_facades_sample_python.clients.version1.ISitesClientV1 import ISitesClientV1
from pip_facades_sample_python.clients.version1.SessionV1 import SessionV1
from pip_facades_sample_python.clients.version1.SiteV1 import SiteV1
from pip_facades_sample_python.clients.version1.UserPasswordInfoV1 import UserPasswordInfoV1
from pip_facades_sample_python.operations.version1.SessionUserV1 import SessionUserV1


class SessionsOperationsV1(RestOperations):
    __default_config = ConfigParams.from_tuples(
        'options.cookie_enabled', True,
        'options.cookie', 'x-session-id',
        'options.max_cookie_age', 365 * 24 * 60 * 60 * 1000
    )

    def __init__(self):
        super().__init__()

        self.__cookie: str = 'x-session-id'
        self.__cookie_enabled: bool = True
        self.__max_cookie_age: float = 365 * 24 * 60 * 60 * 1000
        self.__settings_client: ISettingsClientV1 = None
        self.__accounts_client: IAccountsClientV1 = None
        self.__sessions_client: ISessionsClientV1 = None
        self.__passwords_client: IPasswordsClientV1 = None
        self.__roles_client: IRolesClientV1 = None
        self.__email_settings_client: IEmailSettingsClientV1 = None
        self.__sites_client: ISitesClientV1 = None
        self.__invitations_client: IInvitationsClientV1 = None

        self._dependency_resolver.put('settings', Descriptor('pip-services-settings', 'client', '*', '*', '1.0'))
        self._dependency_resolver.put('accounts', Descriptor('pip-services-accounts', 'client', '*', '*', '1.0'))
        self._dependency_resolver.put('passwords', Descriptor('pip-services-passwords', 'client', '*', '*', '1.0'))
        self._dependency_resolver.put('roles', Descriptor('pip-services-roles', 'client', '*', '*', '1.0'))
        self._dependency_resolver.put('emailsettings',
                                      Descriptor('pip-services-emailsettings', 'client', '*', '*', '1.0'))
        self._dependency_resolver.put('sessions', Descriptor('pip-services-sessions', 'client', '*', '*', '1.0'))
        self._dependency_resolver.put('sites', Descriptor('pip-services-sites', 'client', '*', '*', '1.0'))
        self._dependency_resolver.put('invitations', Descriptor('pip-services-invitations', 'client', '*', '*', '1.0'))

    def configure(self, config: ConfigParams):
        config = config.set_defaults(SessionsOperationsV1.__default_config)
        self._dependency_resolver.configure(config)

        self.__cookie_enabled = config.get_as_boolean_with_default('options.cookie_enabled', self.__cookie_enabled)
        self.__cookie = config.get_as_string_with_default('options.cookie', self.__cookie)
        self.__max_cookie_age = config.get_as_long_with_default('options.max_cookie_age', self.__max_cookie_age)

    def set_references(self, references: IReferences):
        super().set_references(references)

        self.__settings_client = self._dependency_resolver.get_one_required('settings')
        self.__sessions_client = self._dependency_resolver.get_one_required('sessions')
        self.__accounts_client = self._dependency_resolver.get_one_required('accounts')
        self.__passwords_client = self._dependency_resolver.get_one_required('passwords')
        self.__roles_client = self._dependency_resolver.get_one_required('roles')
        self.__email_settings_client = self._dependency_resolver.get_one_required('emailsettings')
        self.__sites_client = self._dependency_resolver.get_one_required('sites')
        self.__invitations_client = self._dependency_resolver.get_one_required('invitations')

    def load_session(self):
        # parse headers first, and if nothing in headers get cookie
        session_id = bottle.request.headers.get('x-session-id')

        session, err = None, None

        if session_id:
            try:
                session = self.__sessions_client.get_session_by_id('facade', session_id)
                if session is None:
                    raise UnauthorizedException(
                        'facade',
                        'SESSION_NOT_FOUND',
                        'Session invalid or already expired.'
                    ).with_details('session_id', session_id).with_status(440)
            except Exception as err:
                return self._send_error(err)

            if err is None and session:
                # Associate session user with the request
                bottle.request.user_id = session.user_id
                bottle.request.user_name = session.user_name
                bottle.request.user = session.user
                bottle.request.session_id = session.id
            else:
                return self._send_error(err)

    def open_session(self, account: AccountV1, roles: List[str], ):
        try:
            session: SessionV1 = None
            sites: List[SiteV1] = None
            passwordInfo: UserPasswordInfoV1 = None
            settings: ConfigParams = None

            # Retrieve sites for user

            site_roles = [] if not roles else list(filter(lambda x: x.find(':') > 0, roles))
            site_ids = [] if not site_roles else list(map(lambda x: x[0:x.find(':')] if x.find(':') >= 0 else x, site_roles))

            if len(site_ids) > 0:
                filter_params = FilterParams.from_tuples('ids', site_ids)
                page = self.__sites_client.get_sites(None, filter_params, None)
                sites = [] if page is None else page.data
            else:
                sites = []

            password_info = self.__passwords_client.get_password_info(None, account.id)

            settings = self.__settings_client.get_section_by_id(None, account.id)

            # Open a new user session
            user = SessionUserV1(
                id=account.id,
                name=account.name,
                login=account.login,
                create_time=account.create_time,
                time_zone=account.time_zone,
                language=account.language,
                theme=account.theme,
                roles=roles,
                sites=list(map(lambda x: {'id': x.id, 'name': x.name}, sites)),
                settings=settings,
                change_pwd_time=None if password_info is None else password_info.change_time,
                custom_hdr=account.custom_hdr,
                custom_dat=account.custom_dat
            )

            address = HttpRequestDetector.detect_address(bottle.request)
            client = HttpRequestDetector.detect_browser(bottle.request)
            platform = HttpRequestDetector.detect_platform(bottle.request)

            session = self.__sessions_client.open_session(None, account.id, account.name, address, client, user, None)
            return JsonConverter.to_json(session)
        except Exception as err:
            return self._send_error(err)

    def signup(self):
        try:
            signup_data = bottle.request.json if isinstance(bottle.request.json, dict) else JsonConverter.from_json(
                TypeCode.Map, bottle.request.json)
            account: AccountV1 = None
            invited: bool = False
            roles: List[str] = []

            # Validate password first
            # Todo: complete implementation after validate password is added

            # Create account
            new_account = AccountV1(
                name=signup_data.get('name'),
                login=signup_data.get('login') or signup_data.get('email'),
                language=signup_data.get('language'),
                theme=signup_data.get('theme'),
                time_zone=signup_data.get('time_zone')
            )

            account = self.__accounts_client.create_account(None, new_account)

            # Create password for the account
            password = signup_data.get('password')

            self.__passwords_client.set_password(None, account.id, password)

            # Activate all pending invitations
            email = signup_data.get('email')

            invitations = self.__invitations_client.activate_invitations(None, email, account.id)
            if invitations:
                # Calculate user roles from activated invitations
                for invitation in invitations:
                    # Was user invited with the same email?
                    invited = invited or email == invitation.invitee_email

                    if invitation.site_id:
                        invitation.role = invitation.role or 'user'
                        role = invitation.site_id + ':' + invitation.role
                        roles.append(role)

            # Create email settings for the account
            new_email_settings = EmailSettingsV1(
                id=account.id,
                name=account.name,
                email=email,
                language=account.language
            )

            if self.__email_settings_client is not None:
                if invited:
                    self.__email_settings_client.set_verified_settings(None, new_email_settings)
                else:
                    self.__email_settings_client.set_settings(None, new_email_settings)

            return self.open_session(account, roles)
        except Exception as err:
            return self._send_error(err)

    def signup_validate(self):
        login = dict(bottle.request.query.decode()).get('login')

        if login:
            try:
                account = self.__accounts_client.get_account_by_id_or_login(None, login)
                if account:
                    raise BadRequestException(
                        None, 'LOGIN_ALREADY_USED',
                        'Login ' + login + ' already being used'
                    ).with_details('login', login)

                return bottle.HTTPResponse(status=204)

            except Exception as err:
                return self._send_error(err)
        else:
            return bottle.HTTPResponse(status=204)

    def signin(self):
        json_data = bottle.request.json if isinstance(bottle.request.json, dict) else JsonConverter.from_json(
            TypeCode.Map, bottle.request.json)

        login = json_data.get('login')
        password = json_data.get('password')

        account: AccountV1 = None
        roles: List[str] = []

        # Find user account
        try:
            account = self.__accounts_client.get_account_by_id_or_login(None, login)

            if account is None:
                raise BadRequestException(
                    None,
                    'WRONG_LOGIN',
                    'Account ' + login + ' was not found'
                ).with_details('login', login)

            # Authenticate user
            result = self.__passwords_client.authenticate(None, account.id, password)
            # wrong password error is UNKNOWN when use http client
            if result is False:
                raise BadRequestException(
                    None,
                    'WRONG_PASSWORD',
                    'Wrong password for account ' + login
                ).with_details('login', login)

            # Retrieve user roles
            if self.__roles_client:
                roles = self.__roles_client.get_roles_by_id(None, account.id, )
            else:
                roles = []

            return self.open_session(account, roles)
        except Exception as err:
            return self._send_error(err)

    def signout(self):
        if hasattr(bottle.request, 'session_id'):
            try:
                self.__sessions_client.close_session(None, bottle.request.session_id)
                return bottle.HTTPResponse(status=204)
            except Exception as err:
                self._send_error(err)

        return bottle.HTTPResponse(status=204)

    def get_sessions(self):
        filter_params = self._get_filter_params()
        paging = self._get_paging_params()

        result = self.__sessions_client.get_sessions(None, filter_params, paging)
        return self._send_result(result)

    def restore_session(self):
        try:
            session_id = dict(bottle.request.query.decode()).get('session_id')

            session = self.__sessions_client.get_session_by_id(None, session_id)

            # If session closed then return null
            if session and not session.active:
                session = None

            if session:
                return JsonConverter.to_json(session)
            else:
                return bottle.HTTPResponse(status=204)
        except Exception as err:
            return self._send_error(err)

    def get_user_sessions(self, user_id):
        filter_params = self._get_filter_params()
        paging = self._get_paging_params()

        filter_params.set_as_object('user_id', user_id)

        sessions = self.__sessions_client.get_sessions(None, filter_params, paging)
        return self._send_result(sessions.to_json())

    def get_current_session(self):
        # parse headers first, and if nothing in headers get cookie
        session_id = bottle.request.headers.get('x-session-id')

        session = self.__sessions_client.get_session_by_id(None, session_id)
        return self._send_result(session)

    def close_session(self, user_id, session_id):

        result = self.__sessions_client.close_session(None, session_id)
        return self._send_result(result)

Not available

This class contains the main operations for managing sessions, which allow us to load existing sessions, create new sessions, close existing sessions, and also authenticate users. This class depends on four microservices - Accounts, Passwords, Roles, and Sessions - each of which is responsible for a certain part of the class’s logic.

The signup and signin methods perform registration of new users and authentication of existing ones, respectively. Upon successful registration or authorization, the handlers of these operations open a new session as the finishing step of these methods.

The signout method closes sessions when users leave the system, or automatically when the session expires.

The load_session interceptor checks a session’s validity, and whether or not it even exists. The interceptor checks the request’s header for a session ID and, if one is found, uses it to retrieve data about the session from the Sessions microservice. If the session is expired or incorrect, an error will be returned. If everything’s all right, information about the user and the session are extracted and attached to the request, and a “green light” is given for further processing.

To perform these operations, our microservice needs to be able to interact with the microservices it depends on. This communication is made simple using standard clients, the links to which are set in the setReferences method.

The authorization mechanism will be responsible for limiting access to resources, depending on the roles a user has been assigned. This part’s implementation will be discussed in Step 5 - Authorization.

Step 5. Authorization