Channel access tokens

Copy Markdown View Source

A channel access token is the Bearer credential every Messaging API call needs:

client = ExLine.Client.new(access_token: token)
ExLine.Api.Messaging.push(client, user_id, ExLine.Message.text("hi"))

You have two choices for where that token comes from:

  1. Just paste one. In the LINE Developers Console, issue a long-lived token and put it in config. Done — you can ignore the rest of this guide.

    config :ex_line, access_token: System.get_env("LINE_CHANNEL_ACCESS_TOKEN")
  2. Issue tokens programmatically with ExLine.Api.ChannelAccessToken, and optionally keep one warm with ExLine.ChannelAccessToken.Cache. This is the rest of the guide.

The three token types

TypeIssue withAuthLifetimeHow many activeVerify/revoke
Long-lived (v1)issue/3channel id + secret~30 days fixed1verify/2, revoke/2
Statelessissue_stateless/3 or issue_stateless_with_jwt/2secret or JWT~15 minunlimited (not stored)
v2.1 (custom expiry)issue_jwt/2JWT assertionup to 30 daysup to 30verify_jwt/2, revoke_jwt/4, key_ids/2

Rules of thumb:

  • One bot, simplest setup → paste a Console long-lived token, or issue/3.
  • Short bursts / serverless → stateless. Issue one, send, forget. No state to manage.
  • Need to rotate without downtime, or many short-lived tokens → v2.1 (up to 30 active tokens with independent expiries).

All of these take a transport-only client — they authenticate through the request body, not a Bearer header, so there is no access_token to provide:

client = ExLine.Client.transport()

Issuing without JWT (channel secret)

The simplest programmatic path. No keys to generate.

client = ExLine.Client.transport()

{:ok, %{"access_token" => token, "expires_in" => secs}} =
  ExLine.Api.ChannelAccessToken.issue_stateless(client, channel_id, channel_secret)

(issue/3 is the same call shape for a long-lived v1 token.)

Issuing with a JWT assertion (stateless or v2.1)

The JWT-based endpoints prove channel ownership with a JWT you sign with a private key, whose public key LINE knows.

1. Generate a key pair

# Private key (keep this secret, give it to ExLine)
openssl genrsa -out line_assertion.pem 2048
# Public key (register this with LINE)
openssl rsa -in line_assertion.pem -pubout -out line_assertion.pub.pem

LINE accepts the public key as a JWK. Convert the PEM to a JWK (one option):

{_, jwk_map} =
  "line_assertion.pub.pem"
  |> File.read!()
  |> JOSE.JWK.from_pem()
  |> JOSE.JWK.to_map()

# add "alg" and "use" as the Console expects
jwk_map = Map.merge(jwk_map, %{"alg" => "RS256", "use" => "sig"})
IO.puts(Jason.encode!(jwk_map))

2. Register the public key in the Console

In the LINE Developers Console, open your Messaging API channel → Basic settings → Assertion Signing Key → Register a public key, paste the JWK, and save. The Console returns a kid (key id). Keep it.

3. Sign assertions with the private key + kid

ExLine.ChannelAccessToken.Assertion.sign/1 builds the RS256 JWT for you:

assertion =
  ExLine.ChannelAccessToken.Assertion.sign(
    channel_id: channel_id,        # used as iss/sub
    kid: kid,                      # from the Console
    private_key: File.read!("line_assertion.pem"),
    token_exp: 2_592_000           # desired token lifetime in seconds (v2.1 only, max 30 days)
  )

client = ExLine.Client.transport()

# v2.1 token (keep key_id to list/inspect later)
{:ok, %{"access_token" => token, "key_id" => key_id}} =
  ExLine.Api.ChannelAccessToken.issue_jwt(client, assertion)

# or a stateless token (sign without :token_exp)
{:ok, %{"access_token" => token}} =
  ExLine.Api.ChannelAccessToken.issue_stateless_with_jwt(client, assertion)

List the valid v2.1 key ids (also JWT-authenticated):

{:ok, %{"kids" => kids}} = ExLine.Api.ChannelAccessToken.key_ids(client, assertion)

Caching and auto-refresh

ExLine.ChannelAccessToken.Cache keeps a token warm and refreshes it before it expires. ExLine owns no processes, so you add it to your supervision tree:

children = [
  {ExLine.ChannelAccessToken.Cache,
   name: :my_channel,
   issue: fn ->
     ExLine.Api.ChannelAccessToken.issue_stateless(
       ExLine.Client.transport(),
       System.fetch_env!("LINE_CHANNEL_ID"),
       System.fetch_env!("LINE_CHANNEL_SECRET")
     )
   end,
   refresh_before: 600}
]

Supervisor.start_link(children, strategy: :one_for_one)

Read the cached token wherever you send messages:

{:ok, token} = ExLine.ChannelAccessToken.Cache.token(:my_channel)
client = ExLine.Client.new(access_token: token)
ExLine.Api.Messaging.push(client, user_id, ExLine.Message.text("hi"))

For multiple channels, start one Cache per channel under different :names. For a v2.1 token, make :issue sign an assertion and call issue_jwt/2 instead.