12

Usually the dart documentation has a lot of useful examples on almost any topic. Unfortunately I could not find anything on sessions in dart.

Could anyone validate this approach as a correct way to do sessions:

  1. Browser sends GET request to sever.
  2. Server responds with web-client.
  3. Web-client sends user credentials.
  4. a) Server checks credentials and generates session cookie. b) Server sends session cookie back to client.
  5. Web-client stores cookie for further use.
  6. Web-client sends request for some user specific data, and attaches the cookie for verification.

My special interest lies in points 4, 5 and 6, since the others are well documented. If you could share some code snippets on this points, I would very much appreciate it.

EDIT: After reading the comment from Günter Zöchbauer below I looked into shelf_auth. I realized that it requires rewriting the server app to use shelf. So I did that.

The main.dart:

// imports of all necessary libraries

main() {
    runServer();
}


/**
 *  Code to handle Http Requests
 */
runServer() {
  var staticHandler = createStaticHandler(r"C:\Users\Lukasz\dart\auctionProject\web", defaultDocument: 'auctionproject.html');
  var handler = new Cascade()
                      .add(staticHandler)  // serves web-client
                      .add(routes.handler) // serves content requested by web-client
                      .handler;
  io.serve(handler, InternetAddress.LOOPBACK_IP_V4, 8080).then((server) {
    print('Listening on port 8080');
  }).catchError((error) => print(error)); 
}

The routes.dart

import 'handlers.dart' as handler;

import 'package:shelf_route/shelf_route.dart';
import 'package:shelf_auth/shelf_auth.dart' as sAuth;

Router routes = new Router()
         ..get('/anonymous', handler.handleAnonymousRequest);
         //..post('/login', handler.handleLoginRequest); << this needs to be implemented
                      //other routs will come later

The handlers.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io' show HttpHeaders;    
import 'databaseUtility.dart';
import 'package:shelf_exception_response/exception.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_path/shelf_path.dart';


shelf.Response handleAnonymousRequest(shelf.Request request) {
  return new shelf.Response.ok('got anonymous get request');
}

Unfortunately after reading the shelf_auth documentation I still don't quite know where to add the authentication. They use the Pipline syntax for the handler.

Lukasz
  • 2,257
  • 3
  • 26
  • 44
  • I use the session handler of https://pub.dartlang.org/packages/shelf_auth for this purpose. – Günter Zöchbauer May 07 '15 at 17:01
  • @GünterZöchbauer could you please take a look at the code above. – Lukasz May 08 '15 at 23:18
  • I won't have time today, maybe tomorrow... – Günter Zöchbauer May 09 '15 at 07:32
  • Sounds about right to me. The critical parts are how you create the session cookies so they can't be guessed/predicted and how you send the cookie (http://resources.infosecinstitute.com/securing-cookies-httponly-secure-flags/). I don't have much knowledge about security though. – Günter Zöchbauer May 12 '15 at 13:58
  • Have a look at this question, it'll explain the security part of what you're looking for, I had to solve the same problem in Java backend + Dart frontend: https://security.stackexchange.com/questions/84860/how-to-build-a-secure-stateless-authentication-system-for-a-client-side-javascri – Jan Vladimir Mostert May 19 '15 at 07:50
  • and also this one: https://stackoverflow.com/questions/29331222/how-to-build-a-secure-stateless-authentication-system-for-a-client-side-dart-app I generate session tokens using a UUID. I'm not sure how to set the cookie on the response as I've never worked with Dart on the server side, but it should give you an idea of what needs to happen on the server side. – Jan Vladimir Mostert May 19 '15 at 07:53

2 Answers2

3

I'll describe how session works in Java with servlets. This could help you in making your implementation work. First off, I have to mention that session and authentication are two separate functions, although the latter depends on the former.

A session helps the server understand consecutive requests coming from the same browser without a big idle time in between. Take a look at the below example:

  1. A user opened a browser A, visited your site
  2. Kept clicking around various links using multiple tabs in browser A
  3. Left the browser idle for 45 minutes
  4. Continued clicking on the pages that he left open
  5. Opened browser B, visited your site
  6. Closed the tab for your website in browser B
  7. Opened another new tab in browser B and clicked on a bookmark to visit your site

Here is the impact on the server-side session for the above steps of the user:

  1. New session created... let us say JSESSIONID 10203940595940
  2. Same session applies for all requests from all tabs
  3. Session expired on the server, and probably some memory was freed on server
  4. Since Java is not able to locate a session matching JSESSIONID 10203940595940, it creates a new session and asks client to remember new JSESSIONID w349374598457
  5. Requests from new browser are treated as new sessions because the JSESSIONID contract is between a single browser and the server. So, server assigns a new JSESSIONID like 956879874358734
  6. JSESSIONID hangs around in the browser till browser is exited. Closing a tab doesnt clear the JSESSIONID
  7. JSESSIONID is still used by browser, and if not much time elapsed, server would still be hanging on to that session. So, the sesison will continue.

Session use on the server-side:

  • A session is just a HashMap which maps JSESSIONIDs with another a bunch of attributes.
  • There is a thread monitoring elapsed time for the sessions, and removing JSESSIONIDs and the mapped attributes from memory once a session expires.
  • Usually, there is some provision for applications to get an event alert just when a session becomes ready for expiry.

Implementation details:

  • User's browser A sends a request to server. Server checks if there is a Cookie by name JSESSIONID. If none is found, one is created on the server. The server makes a note of the new JSESSIONID, the created time, and the last request time which is the same as created time in this case. In the HTTP response, the server attaches the new JSESSIONID as a cookie.
  • Browsers are designed to keep attaching cookies for subsequent visits to the same site. So, for all subsequent visits to the site, the browser keeps attaching the JSESSIONID cookie to the HTTP request header.
  • So, this time the server sees the JSESSIONID and is able to map the request to the existing session, in case the session has not yet expired. In case the session had already expired, the server would create a new session and attach back the new JSESSIONID as a cookie in HTTP response.

Authentication mechanisms just make use of the above session handling to detect "new sessions" and divert them to the login page. Also, existing sessions could be used to store attributes such as "auth-status" - "pass" or "fail".

Teddy
  • 4,009
  • 2
  • 33
  • 55
2

Below is a small example of how this can be achieved (without client).

import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf_session/cookies_middleware.dart';
import 'package:shelf_session/session_middleware.dart';
import 'package:shelf_static/shelf_static.dart';

void main(List<String> args) async {
  final router = Router();
  router.get('/', _handleHome);
  router.get('/login', _handleLogin);
  router.get('/login/', _handleLogin);
  router.post('/login', _handleLogin);
  router.post('/login/', _handleLogin);
  router.get('/logout', _handleLogout);
  router.get('/logout/', _handleLogout);
  final staticHandler =
      createStaticHandler('web', defaultDocument: 'index.html');
  final handler = Cascade().add(staticHandler).add(router).handler;
  final pipeline = const Pipeline()
      .addMiddleware(logRequests())
      .addMiddleware(cookiesMiddleware())
      .addMiddleware(sessionMiddleware())
      .addHandler(handler);
  const address = 'localhost';
  const port = 8080;
  final server = await io.serve(pipeline, address, port);
  print('Serving at http://${server.address.host}:${server.port}');
}

const _menu = '''
<a href="/">Home</a><br />
<a href="/login">Log in</a><br />
<a href="/logout">Log out</a><br />''';

Future<Response> _handleHome(Request request) async {
  final userManager = UserManager();
  final user = userManager.getUser(request);
  var body = '$_menu{{message}}<br />{{cookies}}';
  if (user == null) {
    body = body.replaceAll('{{message}}', 'You are not logged in');
  } else {
    body = body.replaceAll('{{message}}', 'You are logged in as ${user.name}');
  }

  final cookies = request.getCookies();
  body = body.replaceAll('{{cookies}}',
      cookies.entries.map((e) => '${e.key}: ${e.value}').join('<br />'));
  request.addCookie(Cookie('foo', 'Foo'));
  request.addCookie(Cookie('baz', 'Baz'));
  return _render(body);
}

Future<Response> _handleLogin(Request request) async {
  const html = '''
<form action="" method="post">
<label>Login</label><br />
<input name="login" type="text" /><br />
<label>Password</label><br />
<input name="password" type="password" /><br /><br />
<button>Log in</button>
</form>
''';

  if (request.method == 'GET') {
    return _render(_menu + html);
  }

  final body = await request.readAsString();
  final queryParameters = Uri(query: body).queryParameters;
  final login = queryParameters['login'] ?? ''
    ..trim();
  final password = queryParameters['password'] ?? ''
    ..trim();
  if (login.isEmpty || password.isEmpty) {
    return _render(_menu + html);
  }

  final user = User(login);
  final userManager = UserManager();
  userManager.setUser(request, user);
  return Response.found('/');
}

Future<Response> _handleLogout(Request request) async {
  Session.deleteSession(request);
  return Response.found('/');
}

Response _render(String body) {
  return Response.ok(body, headers: {
    'Content-type': 'text/html; charset=UTF-8',
  });
}

class User {
  final String name;

  User(this.name);
}

class UserManager {
  User? getUser(Request request) {
    final session = Session.getSession(request);
    if (session == null) {
      return null;
    }

    final user = session.data['user'];
    if (user is User) {
      return user;
    }

    return null;
  }

  User setUser(Request request, User user) {
    var session = Session.getSession(request);
    session ??= Session.createSession(request);
    session.data['user'] = user;
    return user;
  }
}

For client-server (password implies password hash):
Server code:

class ClientApi {
  final _authenticate = createMiddleware(
    requestHandler: (Request request) async {
      final headers = request.headers;
      final xAuthKey = headers['X-Auth-Key'];
      if (xAuthKey is! String) {
        return Response(401);
      }

      final xAuthEmail = headers['X-Auth-Email'];
      if (xAuthEmail is! String) {
        return Response(401);
      }

      final connection = await getConnection();
      final statement = SelectStatement();
      statement.fields.add('id');
      statement.fields.add('password');
      statement.tables.add('customers');
      statement.where.add('id = ?');
      final rows = await connection.query('$statement;', [xAuthEmail]);
      for (final row in rows) {
        final fields = row.fields;
        final password = fields['password'] as String;
        final apiKey = _getApiKey(password);
        if (xAuthKey == apiKey) {
          return null;
        }
      }

      return Response(401);
    },
  );
  Handler get handler {
    final router = Router();
    final routes = {
      'login': _login,
    };
    for (final key in routes.keys) {
      final value = routes[key]!;
      router.post('/$key', const Pipeline().addHandler(value));
      router.post('/$key/', const Pipeline().addHandler(value));
    }

    final routes2 = {
      'add_to_cart': _addToCart,
    };
    for (final key in routes2.keys) {
      final value = routes2[key]!;
      router.post('/$key',
          const Pipeline().addMiddleware(_authenticate).addHandler(value));
      router.post('/$key/',
          const Pipeline().addMiddleware(_authenticate).addHandler(value));
    }

    return router;
  }

  Future<Response> _login(Request request) async {
    final params = await fromJson(request, LoginRequest.fromJson);
    final connection = await getConnection();
    final name = params.name.toLowerCase();
    final statement = SelectStatement();
    statement.tables.add('customers');
    statement.fields.add('password');
    statement.where.add('id = ?');
    final rows = await connection.query('$statement;', [name]);
    String? password;
    for (final row in rows) {
      final fields = row.fields;
      password = fields['password'] as String;
      break;
    }

    if (password != null && password == params.password) {
      final apiKey = _getApiKey(password);
      final user = LoginUser(
        apiKey: apiKey,
        name: name,
      );
      return toJson(LoginResponse(user: user).toJson());
    }

    return toJson(LoginResponse(user: null).toJson());
  }
}

Client code:

class ClientApi {
  void _addAuthHeaders(Map<String, String> headers) {
    final user1 = UserService.user.value;
    if (user1 != null) {
      headers['X-Auth-Key'] = user1.apiKey;
      headers['X-Auth-Email'] = user1.name;
    }
  }

  Future<LoginResponse> login({
    required String name,
    required String password,
  }) async {
    final request = LoginRequest(
      name: name,
      password: password,
    );
    final json = await _post<Map>(
      'login',
      body: request.toJson(),
    );
    final response = LoginResponse.fromJson(json);
    return response;
  }

  Future<T> _post<T>(
    String path, {
    Map<String, String>? headers,
    Object? body,
  }) async {
    final url = Uri.parse('$_host/$path');
    headers ??= {};
    headers.addAll({
      'Content-type': 'application/json',
    });
    final response = await http.post(
      url,
      body: jsonEncode(body),
      headers: headers,
    );
    if (response.statusCode != HttpStatus.ok) {
      throw StateError('Wrong response status code: ${response.statusCode}');
    }

    final json = jsonDecode(response.body);
    if (json is! T) {
      throw StateError('Wrong response');
    }

    return json;
  }
}

Somewhere in the UI component.

void onClick(Event event) {
  event.preventDefault();
  final bytes = utf8.encode(password);
  final digest = sha256.convert(bytes);
  Timer.run(() async {
    try {
      final clientApi = ClientApi();
      final response =
          await clientApi.login(name: name, password: digest.toString());
      final user = response.user;
      if (user != null) {
        UserService.user.value = User(apiKey: user.apiKey, name: user.name);
      }
    } catch (e) {
      //
    }

    changeState();
  });
}

mezoni
  • 10,684
  • 4
  • 32
  • 54