Skip to main content

better_auth_api/plugins/oauth/
line.rs

1use std::{error::Error, fmt::Write, sync::Arc};
2
3use async_trait::async_trait;
4use better_auth_core::{AuthError, AuthResult};
5use serde_json::{Map, Value};
6
7use super::{
8    GenericOAuthConfig, GenericOAuthProfileContext, GenericOAuthUserInfoHandler,
9    OAuthAccountSubject, OAuthUserInfoRequest,
10};
11
12const VERIFY_URL: &str = "https://api.line.me/oauth2/v2.1/verify";
13
14impl GenericOAuthConfig {
15    /// Configure LINE code login with profile claims from its ID-token verification endpoint.
16    /// Register each channel with its own name through `OAuthPlugin::add_generic_provider`.
17    pub fn line(client_id: &str, client_secret: &str) -> Self {
18        Self {
19            client_id: client_id.into(),
20            client_secret: Some(client_secret.into()),
21            authorization_url: Some("https://access.line.me/oauth2/v2.1/authorize".into()),
22            token_url: Some("https://api.line.me/oauth2/v2.1/token".into()),
23            user_info_url: Some("https://api.line.me/oauth2/v2.1/userinfo".into()),
24            scopes: vec!["openid".into(), "profile".into(), "email".into()],
25            get_user_info: Some(Arc::new(LineProfile {
26                verify_url: VERIFY_URL.into(),
27            })),
28            account_subject: Some(Arc::new(LineSubject)),
29            ..Default::default()
30        }
31    }
32}
33
34struct LineProfile {
35    verify_url: String,
36}
37
38#[async_trait]
39impl GenericOAuthUserInfoHandler for LineProfile {
40    async fn get_user_info(&self, _: &OAuthUserInfoRequest) -> AuthResult<Option<Value>> {
41        Err(AuthError::internal(
42            "LINE requires resolved profile context",
43        ))
44    }
45
46    async fn get_user_info_with_context(
47        &self,
48        tokens: &OAuthUserInfoRequest,
49        context: GenericOAuthProfileContext<'_>,
50    ) -> AuthResult<Option<Value>> {
51        if let Some(claims) = context.verified_claims() {
52            return profile(claims.as_value()).map(Some);
53        }
54        let client = reqwest::Client::new();
55        let request =
56            if let Some(token) = tokens.id_token.as_deref().filter(|token| !token.is_empty()) {
57                let mut form = vec![("id_token", token), ("client_id", context.client_id())];
58                if let Some(nonce) = context.expected_nonce() {
59                    form.push(("nonce", nonce));
60                }
61                client.post(&self.verify_url).form(&form)
62            } else {
63                let endpoint = context
64                    .user_info_url()
65                    .ok_or_else(|| AuthError::internal("Missing LINE userinfo endpoint"))?;
66                client
67                    .get(endpoint)
68                    .bearer_auth(tokens.access_token.as_deref().unwrap_or("undefined"))
69            };
70        let response = request
71            .send()
72            .await
73            .map_err(|error| request_error("request failed", error))?;
74        let success = response.status().is_success();
75        let body = response
76            .text()
77            .await
78            .map_err(|error| request_error("response failed", error))?;
79        if !success || body.is_empty() {
80            return Ok(None);
81        }
82        let Some(claims) = serde_json::from_str::<Option<Value>>(&body)
83            .map_err(|error| AuthError::internal(format!("Invalid LINE response: {error}")))?
84        else {
85            return Ok(None);
86        };
87        profile(&claims).map(Some)
88    }
89}
90
91fn profile(claims: &Value) -> AuthResult<Value> {
92    let claims = claims
93        .as_object()
94        .ok_or_else(|| AuthError::internal("LINE profile must be an object"))?;
95    let mut profile = Map::new();
96    for (from, to) in [
97        ("sub", "sub"),
98        ("name", "name"),
99        ("email", "email"),
100        ("picture", "image"),
101    ] {
102        if let Some(value) = claims.get(from) {
103            let _ = profile.insert(to.into(), value.clone());
104        }
105    }
106    let _ = profile.insert("emailVerified".into(), Value::Bool(false));
107    Ok(Value::Object(profile))
108}
109
110fn request_error(context: &str, error: reqwest::Error) -> AuthError {
111    let error = error.without_url();
112    let mut message = format!("LINE {context}: {error}");
113    let mut cause = error.source();
114    while let Some(error) = cause {
115        let _ = write!(message, ": {error}");
116        cause = error.source();
117    }
118    AuthError::internal(message)
119}
120
121struct LineSubject;
122
123#[async_trait]
124impl OAuthAccountSubject for LineSubject {
125    async fn resolve_subject(
126        &self,
127        _: &OAuthUserInfoRequest,
128        profile: &Value,
129    ) -> AuthResult<String> {
130        Ok(profile
131            .get("sub")
132            .and_then(Value::as_str)
133            .unwrap_or_default()
134            .into())
135    }
136}
137
138#[cfg(test)]
139#[path = "line_tests.rs"]
140mod tests;