An authentication system is a critical component of any application or system that requires secure user interaction. It ensures that only authorized individuals or entities can access specific resources, data, or functionalities.

We often use this process in our apps, but it’s also a good idea to understand all key-aspects of it. One of such aspects - it’s a token(s).

tokens



Access tokens and ID tokens are both commonly used in modern authentication and authorization frameworks such as OAuth 2.0 and OpenID Connect (OIDC). While they might seem similar, their purposes, usage, and contents are distinct.

The abstract protocol scheme of Bearer-like auth:

 +--------+                               +---------------+
 |        |--(A)- Authorization Request ->|   Resource    |
 |        |                               |     Owner     |
 |        |<-(B)-- Authorization Grant ---|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(C)-- Authorization Grant -->| Authorization |
 | Client |                               |     Server    |
 |        |<-(D)----- Access Token -------|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(E)----- Access Token ------>|    Resource   |
 |        |                               |     Server    |
 |        |<-(F)--- Protected Resource ---|               |
 +--------+                               +---------------+

There is also refresh token, whose primary purpose is to refresh id or access token. See RFC 6750 for more.

Comparison

Let’s review the key-diference between id and access tokens.

1. Purpose

Aspect Access Token ID Token
Definition A token used to authorize access to APIs or resources. A token that provides information about the authenticated user.
Main Purpose Grants permissions to access a resource on behalf of the user. Confirms the user’s identity and carries user profile information.
Use Case Sent to APIs to validate access and permissions. Sent to the client (e.g., web app) to authenticate the user.

2. Issued By

Aspect Access Token ID Token
Issuer Authorization Server (OAuth 2.0) Authorization Server (OIDC)
Standard Protocol Part of OAuth 2.0 Part of OpenID Connect (OIDC), an identity layer built on OAuth 2.0.

3. Content

Aspect Access Token ID Token
Data Included - Scopes granted (e.g., read, write).
- Expiry (exp) and issuer (iss).
- Metadata for authorization.
- User’s unique identifier (sub).
- User claims (e.g., email, name).
- Token metadata (exp, iat, iss).
Structure Typically a JWT (JSON Web Token) or opaque string. Always a JWT under OIDC.

4. Expiry and Scope

Aspect Access Token ID Token
Expiration Short-lived (minutes to hours) to reduce security risks. Typically short-lived but can match session duration.
Scope of Use Limited to accessing specific APIs or resources. Limited to the identity verification context.

5. Transmission

Aspect Access Token ID Token
Sent To Resource servers (APIs) to validate access. The client application for user authentication.
Storage Stored securely on the client (e.g., in-memory, secure storage). Stored on the client, often in conjunction with a session or cookie.

6. Validation

Aspect Access Token ID Token
Who Validates? Resource server (API) validates it. Client application validates it.
Validation Data Signature, expiry, and scopes. Signature, expiry, and audience (aud).

7. Example Use Cases

Use Case Access Token ID Token
Mobile App API Used to authorize API requests like fetching user data. Used to display the user’s profile info in the app.
Web App Login Authorizes API calls for resources like dashboards. Verifies the logged-in user’s identity.

Key Differences in Summary

Feature Access Token ID Token
What it represents Permissions to access resources. Identity of the user.
Who consumes it? APIs or resource servers. Client applications (e.g., SPAs, mobile apps).
Standard OAuth 2.0 OpenID Connect (OIDC).

When to Use Which?

Bearer authentication can use both Access Tokens and ID Tokens because they serve complementary purposes in modern authentication and authorization systems, especially in protocols like OAuth 2.0 and OpenID Connect (OIDC).

  • Access Token: Use it whenever you need to call APIs or access a resource on behalf of a user.
  • ID Token: Use it to authenticate the user and retrieve their identity-related claims.

Together, access tokens and ID tokens work seamlessly to provide a secure and user-friendly experience in modern applications.

Bearer authentication uses both tokens because they address different aspects of security:

  • The Access Token focuses on authorizing access to resources.
  • The ID Token focuses on verifying and communicating user identity to the client.

This dual-token approach ensures better security, flexibility, and user experience in modern distributed systems.

Sometimes we can faced with systems, where id and access tokens are mixed in their responsibilities… but this is a bit another story.

Model

In apps, I developing, I like to use a model, that not only wrap token itself, but also provide some usefull info from it, like jwt.io does.

Note: this is not applicable for refreshToken kind, we can only wrap this token. Why? Because of design - it’s simple created not for u, so no information available without special key.. - it’s for one, that provide this token to u. For us - it’s just a shortcut to refresh access to some resources and information.

The model looks like this

import Foundation

/// Represent JSON WebToken for OpenID
///
/// JWTs (JSON Web Tokens) are split into three pieces (access, id):

/// - **Header** - Provides information about how to validate the token including
///  information about the type of token and how it was signed.
/// - **Payload** - Contains all of the important data about the user
/// or app that is attempting to call your service.
/// - **Signature** - Is the raw material used to validate the token.
///
/// Each piece is separated by a period (.) and separately Base64 encoded.
///
public struct JSONWebToken: Codable, Equatable, Hashable {
  public static func == (lhs: JSONWebToken, rhs: JSONWebToken) -> Bool {
    lhs.raw == rhs.raw
  }

  public enum Kind: String, CaseIterable, Codable, Equatable, Hashable {
    case access
    case refresh
    case id
  }

  public enum Failure: Swift.Error {
    case unexpectedFormat
    case tokenPartsInvalidURLDecode
    case invalidJSON
  }

  // MARK: - Raw

  public let type: Kind
  public let raw: String

  // MARK: - Components

  /// information about how to validate the token including
  /// information about the type of token and how it was signed
  var header: [String: Any]? {
    let parts = raw.components(separatedBy: ".")
    if parts.count == 3,
       let rawHeaderData = parts[0].base64UrlDecode {
      return try? rawHeaderData.decodeJWTPart()
    }
    return nil
  }

  /// Contains all of the important data about the user or app that
  /// is attempting to call your service.
  ///
  /// [read more here](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens#payload-claims)
  var body: [String: Any]? {
    let parts = raw.components(separatedBy: ".")
    if parts.count == 3,
       let rawBodyData = parts[1].base64UrlDecode {
      return try? rawBodyData.decodeJWTPart()
    }
    return nil
  }

  /// Is the raw material used to validate the token
  var signature: String? {
    let parts = raw.components(separatedBy: ".")
    if parts.count == 3 {
      return parts[2]
    }
    return nil
  }

  // MARK: - Values

  public var signAlgorithm: String? {
    header?["alg"] as? String
  }

  public var thumbprint: String? {
    header?["kid"] as? String
  }

  public var issuer: String? {
    body?["iss"] as? String
  }

  public var subject: String? {
    body?["sub"] as? String
  }

  public var audience: String? {
    body?["aud"] as? String
  }

  public var name: String? {
    body?["name"] as? String
  }

  public var givenName: String? {
    body?["given_name"] as? String
  }

  public var familyName: String? {
    body?["family_name"] as? String
  }

  public var identifier: String? {
    body?["jti"] as? String
  }

  public var scopes: String? {
    body?["scp"] as? String
  }

  public var version: String? {
    body?["ver"] as? String
  }

  public var tenantId: String? {
    body?["tid"] as? String
  }

  public var expiresAt: Date? {
    claimAsDate(for: "exp")
  }

  public var issuedAt: Date? {
    claimAsDate(for: "iat")
  }

  public var notBefore: Date? {
    claimAsDate(for: "nbf")
  }

  public var isExpired: Bool {
    if type == .access || type == .id,
       let expiresAt = self.expiresAt {
      return expiresAt.compare(Date()) != .orderedDescending
    } else {
      return false
    }
  }

  // MARK: - Lifecycle

  public init(
    raw: String,
    type: Kind
  ) throws {
    let parts = raw.components(separatedBy: ".")
    if parts.count >= 3 { 
      self.raw = raw
      self.type = type
    } else {
      throw Failure.unexpectedFormat
    }
  }

  // MARK: - Private

  private func claimAsDate(for key: String) -> Date? {
    if let timeStamp = body?[key] as? TimeInterval {
      return Date(timeIntervalSince1970: timeStamp)
    }

    return nil
  }
}

fileprivate extension Data {
  func decodeJWTPart() throws -> [String: Any] {
    let json = try JSONSerialization.jsonObject(with: self, options: [])
    return json as? [String: Any] ?? [: ]
  }
}

fileprivate extension String {
  var base64UrlDecode: Data? {
    var base64 = self
      .replacingOccurrences(of: "-", with: "+")
      .replacingOccurrences(of: "_", with: "/")

    let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8))
    let requiredLength = 4 * ceil(length / 4.0)
    let paddingLength = requiredLength - length

    if paddingLength > 0 {
      let padding = "".padding(
        toLength: Int(paddingLength),
        withPad: "=",
        startingAt: 0
      )
      base64 += padding
    }

    return Data(
      base64Encoded: base64,
      options: .ignoreUnknownCharacters
    )
  }
}

extension JSONWebToken: TokenRepresentable {
  public var accessToken: String {
    raw
  }
}


Using it, we can easely inspect different aspect of the token at any moment like if it’s expired - token.isExpired.

Conclusion

In ideal world, we must use both tokens:

  • Separation of Concerns: Ensures distinct roles for authentication and authorization.
  • Improved Security: Limits the exposure of sensitive identity data.
  • Enhanced Flexibility: Supports diverse use cases across distributed systems.

are a key reasons.

This dual-token system is foundational in modern secure and scalable authentication architectures.

P.S:

“Safety doesn’t happen by accident” - These words by author and motivational speaker, Zig Ziglar.

Resources