diff --git a/src/main/java/run/halo/app/identity/authentication/verifier/AbstractOAuth2TokenAuthenticationToken.java b/src/main/java/run/halo/app/identity/authentication/verifier/AbstractOAuth2TokenAuthenticationToken.java
new file mode 100644
index 000000000..e9a7f14ad
--- /dev/null
+++ b/src/main/java/run/halo/app/identity/authentication/verifier/AbstractOAuth2TokenAuthenticationToken.java
@@ -0,0 +1,92 @@
+package run.halo.app.identity.authentication.verifier;
+
+import java.util.Collection;
+import java.util.Map;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.Assert;
+
+/**
+ * Base class for {@link AbstractAuthenticationToken} implementations that expose common
+ * attributes between different OAuth 2.0 Access Token Formats.
+ *
+ * <p>
+ * For example, a {@link Jwt} could expose its {@link Jwt#getClaims() claims} via
+ * {@link #getTokenAttributes()} or an &quot;Introspected&quot; OAuth 2.0 Access Token
+ * could expose the attributes of the Introspection Response via
+ * {@link #getTokenAttributes()}.
+ *
+ * @author guqing
+ * @see OAuth2AccessToken
+ * @see Jwt
+ * @see <a href="https://tools.ietf.org/search/rfc7662#section-2.2">2.2 Introspection Response</a>
+ * @since 2.0.0
+ */
+public abstract class AbstractOAuth2TokenAuthenticationToken<T extends AbstractOAuth2Token>
+    extends AbstractAuthenticationToken {
+
+    private Object principal;
+
+    private Object credentials;
+
+    private T token;
+
+    /**
+     * Sub-class constructor.
+     */
+    protected AbstractOAuth2TokenAuthenticationToken(T token) {
+
+        this(token, null);
+    }
+
+    /**
+     * Sub-class constructor.
+     *
+     * @param authorities the authorities assigned to the Access Token
+     */
+    protected AbstractOAuth2TokenAuthenticationToken(T token,
+        Collection<? extends GrantedAuthority> authorities) {
+
+        this(token, token, token, authorities);
+    }
+
+    protected AbstractOAuth2TokenAuthenticationToken(T token, Object principal, Object credentials,
+        Collection<? extends GrantedAuthority> authorities) {
+
+        super(authorities);
+        Assert.notNull(token, "token cannot be null");
+        Assert.notNull(principal, "principal cannot be null");
+        this.principal = principal;
+        this.credentials = credentials;
+        this.token = token;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return this.credentials;
+    }
+
+    /**
+     * Get the token bound to this {@link Authentication}.
+     */
+    public final T getToken() {
+        return this.token;
+    }
+
+    /**
+     * Returns the attributes of the access token.
+     *
+     * @return a {@code Map} of the attributes in the access token.
+     */
+    public abstract Map<String, Object> getTokenAttributes();
+
+}
diff --git a/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthentication.java b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthentication.java
new file mode 100644
index 000000000..e97c3c26b
--- /dev/null
+++ b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthentication.java
@@ -0,0 +1,47 @@
+package run.halo.app.identity.authentication.verifier;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.Transient;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link org.springframework.security.core.Authentication} token that represents a
+ * successful authentication as obtained through a bearer token.
+ *
+ * @author guqing
+ * @since 2.0.0
+ */
+@Transient
+public class BearerTokenAuthentication
+    extends AbstractOAuth2TokenAuthenticationToken<OAuth2AccessToken> {
+    private final Map<String, Object> attributes;
+
+    /**
+     * Constructs a {@link BearerTokenAuthentication} with the provided arguments
+     *
+     * @param principal The OAuth 2.0 attributes
+     * @param credentials The verified token
+     * @param authorities The authorities associated with the given token
+     */
+    public BearerTokenAuthentication(OAuth2AuthenticatedPrincipal principal,
+        OAuth2AccessToken credentials,
+        Collection<? extends GrantedAuthority> authorities) {
+        super(credentials, principal, credentials, authorities);
+        Assert.isTrue(credentials.getTokenType() == OAuth2AccessToken.TokenType.BEARER,
+            "credentials must be a bearer token");
+        this.attributes =
+            Collections.unmodifiableMap(new LinkedHashMap<>(principal.getAttributes()));
+        setAuthenticated(true);
+    }
+
+    @Override
+    public Map<String, Object> getTokenAttributes() {
+        return this.attributes;
+    }
+}
diff --git a/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationToken.java b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationToken.java
new file mode 100644
index 000000000..c0606f926
--- /dev/null
+++ b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationToken.java
@@ -0,0 +1,49 @@
+package run.halo.app.identity.authentication.verifier;
+
+import java.util.Collections;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} that contains a
+ * <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>.
+ *
+ * @author guqing
+ * @since 2.0.0
+ */
+public class BearerTokenAuthenticationToken extends AbstractAuthenticationToken {
+    private final String token;
+
+    /**
+     * Create a {@code BearerTokenAuthenticationToken} using the provided parameter(s)
+     *
+     * @param token - the bearer token
+     */
+    public BearerTokenAuthenticationToken(String token) {
+        super(Collections.emptyList());
+        Assert.hasText(token, "token cannot be empty");
+        this.token = token;
+    }
+
+    /**
+     * Get the
+     * <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
+     *
+     * @return the token that proves the caller's authority to perform the
+     * {@link jakarta.servlet.http.HttpServletRequest}
+     */
+    public String getToken() {
+        return this.token;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return this.getToken();
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return this.getToken();
+    }
+}
diff --git a/src/test/java/run/halo/app/authentication/verifyer/BearerTokenAuthenticationTest.java b/src/test/java/run/halo/app/authentication/verifyer/BearerTokenAuthenticationTest.java
new file mode 100644
index 000000000..23a08187a
--- /dev/null
+++ b/src/test/java/run/halo/app/authentication/verifyer/BearerTokenAuthenticationTest.java
@@ -0,0 +1,142 @@
+package run.halo.app.authentication.verifyer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+import com.nimbusds.jose.shaded.json.JSONObject;
+import java.net.URL;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import run.halo.app.identity.authentication.verifier.BearerTokenAuthentication;
+
+/**
+ * Tests for {@link BearerTokenAuthentication}
+ *
+ * @author guqing
+ * @since 2.0.0
+ */
+public class BearerTokenAuthenticationTest {
+    private final OAuth2AccessToken
+        token = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token",
+        Instant.now(), Instant.now().plusSeconds(3600));
+
+    private final String name = "sub";
+
+    private final Map<String, Object> attributesMap = new HashMap<>();
+
+    private DefaultOAuth2AuthenticatedPrincipal principal;
+
+    private final Collection<GrantedAuthority> authorities =
+        AuthorityUtils.createAuthorityList("USER");
+
+    @BeforeEach
+    public void setUp() {
+        this.attributesMap.put(OAuth2TokenIntrospectionClaimNames.SUB, this.name);
+        this.attributesMap.put(OAuth2TokenIntrospectionClaimNames.USERNAME, "username");
+        this.principal = new DefaultOAuth2AuthenticatedPrincipal(this.attributesMap, null);
+    }
+
+    @Test
+    public void getNameWhenConfiguredInConstructorThenReturnsName() {
+        OAuth2AuthenticatedPrincipal principal =
+            new DefaultOAuth2AuthenticatedPrincipal(this.name, this.attributesMap,
+                this.authorities);
+        BearerTokenAuthentication authenticated =
+            new BearerTokenAuthentication(principal, this.token,
+                this.authorities);
+        assertThat(authenticated.getName()).isEqualTo(this.name);
+    }
+
+    @Test
+    public void getNameWhenHasNoSubjectThenReturnsNull() {
+        OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(
+            Collections.singletonMap("claim", "value"), null);
+        BearerTokenAuthentication authenticated =
+            new BearerTokenAuthentication(principal, this.token, null);
+        assertThat(authenticated.getName()).isNull();
+    }
+
+    @Test
+    public void getNameWhenTokenHasUsernameThenReturnsUsernameAttribute() {
+        BearerTokenAuthentication authenticated =
+            new BearerTokenAuthentication(this.principal, this.token, null);
+        assertThat(authenticated.getName())
+            .isEqualTo(this.principal.getAttribute(OAuth2TokenIntrospectionClaimNames.SUB));
+    }
+
+    @Test
+    public void constructorWhenTokenIsNullThenThrowsException() {
+        assertThatIllegalArgumentException()
+            .isThrownBy(() -> new BearerTokenAuthentication(this.principal, null, null))
+            .withMessageContaining("token cannot be null");
+    }
+
+    @Test
+    public void constructorWhenCredentialIsNullThenThrowsException() {
+        assertThatIllegalArgumentException()
+            .isThrownBy(() -> new BearerTokenAuthentication(null, this.token, null))
+            .withMessageContaining("principal cannot be null");
+    }
+
+    @Test
+    public void constructorWhenPassingAllAttributesThenTokenIsAuthenticated() {
+        OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal("harris",
+            Collections.singletonMap("claim", "value"), null);
+        BearerTokenAuthentication authenticated =
+            new BearerTokenAuthentication(principal, this.token, null);
+        assertThat(authenticated.isAuthenticated()).isTrue();
+    }
+
+    @Test
+    public void getTokenAttributesWhenHasTokenThenReturnsThem() {
+        BearerTokenAuthentication authenticated =
+            new BearerTokenAuthentication(this.principal, this.token,
+                Collections.emptyList());
+        assertThat(authenticated.getTokenAttributes()).isEqualTo(this.principal.getAttributes());
+    }
+
+    @Test
+    public void getAuthoritiesWhenHasAuthoritiesThenReturnsThem() {
+        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("USER");
+        BearerTokenAuthentication authenticated =
+            new BearerTokenAuthentication(this.principal, this.token,
+                authorities);
+        assertThat(authenticated.getAuthorities()).isEqualTo(authorities);
+    }
+
+    @Test
+    public void constructorWhenDefaultParametersThenSetsPrincipalToAttributesCopy() {
+        JSONObject attributes = new JSONObject();
+        attributes.put("active", true);
+        OAuth2AuthenticatedPrincipal
+            principal = new DefaultOAuth2AuthenticatedPrincipal(attributes, null);
+        BearerTokenAuthentication token =
+            new BearerTokenAuthentication(principal, this.token, null);
+        assertThat(token.getPrincipal()).isNotSameAs(attributes);
+        assertThat(token.getTokenAttributes()).isNotSameAs(attributes);
+    }
+
+    @Test
+    public void toStringWhenAttributesContainsURLThenDoesNotFail() throws Exception {
+        JSONObject attributes =
+            new JSONObject(Collections.singletonMap("iss", new URL("https://idp.example.com")));
+        OAuth2AuthenticatedPrincipal principal =
+            new DefaultOAuth2AuthenticatedPrincipal(attributes, null);
+        BearerTokenAuthentication token =
+            new BearerTokenAuthentication(principal, this.token, null);
+        assertThat(token.toString()).isNotNull();
+    }
+
+}
diff --git a/src/test/java/run/halo/app/authentication/verifyer/BearerTokenAuthenticationTokenTest.java b/src/test/java/run/halo/app/authentication/verifyer/BearerTokenAuthenticationTokenTest.java
new file mode 100644
index 000000000..a809b73b3
--- /dev/null
+++ b/src/test/java/run/halo/app/authentication/verifyer/BearerTokenAuthenticationTokenTest.java
@@ -0,0 +1,37 @@
+package run.halo.app.authentication.verifyer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+import org.junit.jupiter.api.Test;
+import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationToken;
+
+/**
+ * Tests for {@link BearerTokenAuthenticationToken}
+ *
+ * @author guqing
+ * @since 2.0.0
+ */
+public class BearerTokenAuthenticationTokenTest {
+    @Test
+    public void constructorWhenTokenIsNullThenThrowsException() {
+        assertThatIllegalArgumentException()
+            .isThrownBy(() -> new BearerTokenAuthenticationToken(null))
+            .withMessageContaining("token cannot be empty");
+    }
+
+    @Test
+    public void constructorWhenTokenIsEmptyThenThrowsException() {
+        assertThatIllegalArgumentException()
+            .isThrownBy(() -> new BearerTokenAuthenticationToken(""))
+            .withMessageContaining("token cannot be empty");
+    }
+
+    @Test
+    public void constructorWhenTokenHasValueThenConstructedCorrectly() {
+        BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token");
+        assertThat(token.getToken()).isEqualTo("token");
+        assertThat(token.getPrincipal()).isEqualTo("token");
+        assertThat(token.getCredentials()).isEqualTo("token");
+    }
+}