---
title: 'Authentication and Authorization'
source: 'https://academia.sh/en/courses/mvc-framework/authentication-and-authorization'
course: 'The Model–View–Controller Framework'
language: en
updated: '2026-10-21T05:03:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates authentication from authorization through the stay of the identity and the stay of the decision'
    - 'Measures with the stay rule that a user id written to the session stays bound to the client'
    - 'Builds a permission check derived from the identity as a decision recomputed on every request'
    - 'Classifies with the stay rule the request in which a sign-out removes the identity'
---

# Authentication and Authorization

Measures how many requests a signed-in user's identity stays for, and why the authorization decision is recomputed on every request.

The earlier lessons showed a value that is born inside a request and leaves with it: the path parameter, the validation error, the query cache and the controller's instance variable. The query cache, despite its name, was in the **this request** stay; the second request started the counter from zero. The flash message (`flash`) passed to the next request only. This lesson measures, for the first time, a value that stays in the session, for every later request of the same client. The question is this: after a user signs in, how does every later page know who that user is?

**Authentication** is the work of establishing who sent the request; its result is an identity. **Authorization** is the work of establishing whether that identity may perform a given action; its result is a yes or a no. The two are often mentioned together, but they behave differently in time, and this lesson measures that difference. The measuring axis is the course's axis: **how many requests does this value stay for?** There are four stays: **this request**, **next request**, **session** and **persistent**. M11/K03 also set persistent against session, but the question there was who can see a value. Here nobody is asked who sees what; the question is time. After the request that writes a value, how many more requests see it? M12/K03 measured who can change a result; here the one who changes it does not matter, only the stay is measured.

## Core

The measurements in this lesson rest on a small request loop. The file below is the course core and is given as it is. `App` holds the store shared by all requests and all clients; every request receives a new `Context`; and `session` is a hash chosen per client. `stay(write, read)` writes a value in the first request, reads it in two more requests of the same client and in one request of another client, looks at where the value was seen and classifies the stay.

```ruby
# stay.rb — a small request loop and a helper that classifies how many requests a value stays for

# Flash: a value written in this request is carried to the next request only.
class Flash
  def initialize(incoming)
    @incoming = incoming
    @fresh = {}
  end

  def [](key) = @fresh.fetch(key) { @incoming[key] }

  def []=(key, value)
    @fresh[key] = value
  end

  def passed_on = @fresh
end

# Every request gets a new context: params, flash and the request store start empty each time.
class Context
  attr_reader :app, :params, :session, :flash, :request_store

  def initialize(app, session)
    @app = app
    @params = {}
    @session = session
    @flash = Flash.new(session.delete(:flash) || {})
    @request_store = {}
  end
end

# The application store is the same across all requests and all clients;
# the session is chosen per client.
class App
  attr_reader :store

  def initialize
    @sessions = Hash.new { |h, client| h[client] = {} }
    @store = { products: {}, schema: { products: %i[name price] }, templates: {},
               queue: [], cache: {}, counter: 0 }
  end

  def request(client)
    context = Context.new(self, @sessions[client])
    result = yield context
    context.session[:flash] = context.flash.passed_on unless context.flash.passed_on.empty?
    result
  end
end

# Rule: request 1 (client A) writes the value; requests 2 and 3 (A) and request 4 (B) read it.
# If B sees it, persistent; if A's third request sees it, session; if only the second
# request sees it, next; if none sees it, this_request.
def stay(write, read)
  app = App.new
  first = app.request("A") { |b| write.call(b); read.call(b) }
  raise ArgumentError, "value could not be read in the request that wrote it" if first.nil?

  seen = [["A", 2], ["A", 3], ["B", 4]].map do |client, order|
    app.request(client) { |b| read.call(b) }.nil? ? "--" : "#{client}#{order}"
  end
  kind = if seen[2] != "--" then "persistent"
         elsif seen[1] != "--" then "session"
         elsif seen[0] != "--" then "next"
         else "this_request"
         end
  [kind, ["A1", *seen].join(" "), first]
end
```

This block is source; the file is written to disk and pulled in with `require_relative`. The core is not a framework but a model of one: `session` is a hash, and the persistent store lives in process memory. In a real application the session works through a cookie and a store, and the persistent store works through a database or a cache store; the rule reads how the value behaves, not where it is stored. The measurements are meaningful within a single application process.

## The Stay of Identity

Signing in means writing a user id to the session. Signing out means deleting it. This is why an identity is verified once and kept in the session instead of being asked for on every request: later requests do not receive the password again, they look at the identity in the session. The excerpt below defines the sign-in, sign-out and identity lookup helpers, as well as a permission table.

```ruby
# identity.rb — sign-in, sign-out, identity lookup and the permission derived from identity
require_relative "stay"

PERMISSIONS = { 42 => %i[show edit], 7 => %i[show] }

def sign_in(b, user_id)
  b.session[:user_id] = user_id
end

def sign_out(b) = b.session.delete(:user_id)

def identity(b) = b.session[:user_id]

def allowed?(b, action) = (PERMISSIONS[identity(b)] || []).include?(action)

def report(name, kind, evidence, value)
  printf("%-14s%-14s%-13s%s\n", name, kind, evidence, value.inspect)
end
```

This block is source as well. `PERMISSIONS` is a table from an identity to a list of permitted actions; `allowed?` reads the identity from the session, looks it up in the table and returns a boolean. For someone whose identity is not known at all, `PERMISSIONS[nil]` returns `nil`, `|| []` turns it into an empty list and the result is `false`. Now the first observation: the user id written to the session.

```ruby
# user.rb — how many requests does the user id stay for
require_relative "identity"

write = ->(b) { b.session[:user_id] = 42 }
read = ->(b) { b.session[:user_id] }
report("identity", *stay(write, read))
```
```
identity      session       A1 A2 A3 --  42
```

The identity appeared in all three requests of A and did not appear in the request of B. This is the **session** stay: the value stays for every request of the client that wrote it and does not pass to other clients. That it is not persistent is a deliberate outcome. Had the identity been written to the shared store of the application, B would also have appeared with A's identity. **AS1.** The session staying for A's requests was measured only across three requests; when the session ends, through a timeout or the deletion of the cookie, is not modeled in this core.

## Every Client Has Its Own Identity

Because the session is chosen per client, two clients can work in the same application with different identities and neither sees the other's. In the measurement below A signs in as user 42 and B as user 7; then each client reads its identity. In the last step B signs out and A's identity is looked at again.

```ruby
# clients.rb — two clients, two identities
require_relative "identity"

app = App.new
app.request("A") { |b| sign_in(b, 42) }
app.request("B") { |b| sign_in(b, 7) }
["A", "B", "A"].each do |client|
  puts "#{client} #{app.request(client) { |b| identity(b) }.inspect}"
end
app.request("B") { |b| sign_out(b) }
puts "after sign-out A #{app.request("A") { |b| identity(b) }.inspect}"
puts "after sign-out B #{app.request("B") { |b| identity(b) }.inspect}"
```
```
A 42
B 7
A 42
after sign-out A 42
after sign-out B nil
```

Each client read only its own identity, and B's sign-out did not touch A's session. This is the practical meaning of the identity being in the session stay: the value is bound to the client, not to the shared store of the application. This separation is one of the foundations that keep the many users connected to the same server process from getting mixed up; the measurement was made with two clients.

## The Permission Decision Is Recomputed on Every Request

Since the identity stays in the session, does the authorization decision stay as well? The decision is derived from the identity and the derivation is cheap; the lesson treats writing it to the session or not as a choice. First, look at the derivation itself: two users and a client that has not signed in are asked the same two actions.

```ruby
# permissions.rb — one identity, two actions, three clients
require_relative "identity"

app = App.new
app.request("A") { |b| sign_in(b, 42) }
app.request("B") { |b| sign_in(b, 7) }

["A", "B", "C"].each do |client|
  result = app.request(client) { |b| [allowed?(b, :show), allowed?(b, :edit)] }
  puts "#{client} #{result.inspect}"
end
```
```
A [true, true]
B [true, false]
C [false, false]
```

The same two questions gave three different answers, and the difference came only from the identity in the session. C never signed in, so it was refused for both actions. Authorization is a separate step that reads the value authentication produced: without an identity there is no decision, but the decision is not contained in the identity either.

Now measure the stay of the decision. The decision is computed in the request that signs in and is put only in the request store of that request.

```ruby
# decision.rb — how many requests does the permission decision stay for
require_relative "identity"

write = lambda do |b|
  sign_in(b, 42)
  b.request_store[:decision] = allowed?(b, :edit)
end
read = ->(b) { b.request_store[:decision] }
report("access check", *stay(write, read))
```
```
access check  this_request  A1 -- -- --  true
```

The decision is in the **this request** stay. In the next request the request store starts empty and the decision is computed again. The practical consequence is this: if a user's permission changes, the next request runs with the new permission; there is no copy that carries the old decision. Had the decision been written to the session, a change in the permission table would stay invisible until that session ended. **AS2.** Here the decision is a cheap table lookup. Storing a decision whose computation is expensive is a separate design question, and the next two lessons, on jobs and on caching, treat it from the point of view of the stay.

## If We Write the Decision to the Session

Above, recomputing the authorization decision on every request was treated as a choice. To measure the result of that choice, try the other way: the decision is computed in the request that signs in and is written to the session together with the identity.

```ruby
# session_decision.rb — how many requests does a permission decision written to the session stay for
require_relative "identity"

write = lambda do |b|
  sign_in(b, 42)
  b.session[:decision] = allowed?(b, :edit)
end
read = ->(b) { b.session[:decision] }
report("stored check", *stay(write, read))
```
```
stored check  session       A1 A2 A3 --  true
```

The decision moved to the **session** stay like the identity: it appears in all later requests of the same client. A longer stay does not make a decision correct. To see this, change the permission table after the decision is written and put the two ways side by side: the decision stored in the session and the decision recomputed on every request.

```ruby
# stale_decision.rb — the stored decision and the recomputed decision after the permission table changes
require_relative "identity"

app = App.new
app.request("A") do |b|
  sign_in(b, 42)
  b.session[:decision] = allowed?(b, :edit)
end
PERMISSIONS[42] = %i[show]
stored = app.request("A") { |b| b.session[:decision] }
recomputed = app.request("A") { |b| allowed?(b, :edit) }
puts "stored #{stored}, recomputed #{recomputed}"
```
```
stored true, recomputed false
```

Although the permission was taken back, the decision in the session kept saying the old value; the recomputed decision showed the new situation. Staleness is the loss of the link between a value whose stay has been lengthened and its source. We will meet the same problem for a fragment in the lesson on caching. This is also why the identity is kept in the session and the decision is not: the identity is a fact that does not change until the user signs out (or until the session ends), whereas the decision depends on something else besides the identity, namely the permission table.

## Not Signed In and an Unrecognized Identity

Because the authorization decision is derived from the identity, there are two cases in which no identity is found: the client has never signed in, or the identity in the session does not appear in the permission table. In both cases `allowed?` has to decide instead of raising an error. The measurement below puts three clients side by side: a client that has not signed in, user 99, who is not in the table, and user 42, who is.

```ruby
# unknown.rb — the permission decision for clients with no identity and for an unrecognized one
require_relative "identity"

app = App.new
app.request("B") { |b| sign_in(b, 99) }
app.request("C") { |b| sign_in(b, 42) }
%w[A B C].each do |client|
  who, verdict = app.request(client) { |b| [identity(b), allowed?(b, :show)] }
  puts "#{client} #{who.inspect} #{verdict}"
end
```
```
A nil false
B 99 false
C 42 true
```

The client that has not signed in has an empty identity and the decision is negative; user 99 has an identity, but since the table has no entry for it the decision is again negative. Identity and permission are two separate questions: the first is "who is this client?", the second "can this person do this?". An identity being present in the session does not mean a permission is present. Because the decision is read from the table, every identity that is not in the table remains unauthorized by default.

## Signing Out

Signing out means deleting the identity from the session. Our rule writes only in the first request; to measure the sign-out, the `read` lambda keeps its own call count and, in the second request, reads the identity and then deletes it. The first and second requests see the identity; the third and fourth do not.

```ruby
# signout.rb — what happens to the stay if the sign-out comes in the second request
require_relative "identity"

write = ->(b) { sign_in(b, 42) }
calls = 0
read = lambda do |b|
  calls += 1
  value = identity(b)
  sign_out(b) if calls == 2
  value
end
report("sign out", *stay(write, read))
```
```
sign out      next          A1 A2 -- --  42
```

The rule classified this value as **next request**, because only the second request saw it. The same identity, without the sign-out, was in the **session** stay. This is not a contradiction but evidence that the rule reads behavior: the value is stored in the session, yet the sign-out deletes it early and the stay becomes shorter. That is the purpose of a sign-out. **AS3.** Having the sign-out occur in the second request is the arrangement of the measurement; in a real application nobody knows in advance in which request the sign-out will arrive.

## The Bounding Measurement

The rule of this lesson carries a limit as well. If `stay` cannot read the value even in the request that wrote it, it does not classify; it raises an exception. Measure this with an arrangement in which the writing side writes nothing.

```ruby
# boundary.rb — what does the rule do if no value is written
require_relative "identity"

begin
  stay(->(b) { }, ->(b) { identity(b) })
rescue => e
  puts e.class
end
```
```
ArgumentError
```

The rule raised `ArgumentError`. That is, **a value that cannot be read in the request that wrote it cannot be classified into any stay**; the stay takes at least one appearance of the value as a precondition. The identity of a client that has not signed in therefore carries no stay: it does not exist. All four measurements of the lesson stayed inside this limit. The identity stayed in the session, the decision in this request, the decision written to the session in the session, and after a sign-out the identity in the next request. The limit is also a warning: before measuring the stay of a value, make sure the value really exists in the first request. If a `read` lambda that reads the identity looks at the wrong key, the answer of the rule is not a stay but an error.

## Summary

- Authentication produces an identity, authorization derives a yes or a no from that identity; the two are separate steps.
- A user id written to the session appears in all requests of the client that wrote it and does not appear for another client; two clients work in the same application with different identities. The stay of this lesson is **session**.
- If the permission decision is recomputed from the identity on every request, it is in the **this request** stay and a permission change appears in the next request; if it is written to the session, its stay extends to the session and it goes stale when the permission changes.
- A client that has not signed in, or an identity that is not in the table, remains unauthorized by default; an identity being present does not show that a permission is present.
- The request that signs out deletes the identity from the session; after the deleting request the identity is gone and the rule sees it as **next request**.
- `stay` does not classify a value that cannot be read in the request that wrote it, and raises `ArgumentError`.

## Next Step

The identity stays in the session and the decision is produced again on every request; but where does a piece of work that must run after the request has finished wait? The next lesson measures background jobs: how many requests a job placed in a queue stays for, and why that stay differs from the session.
