better_auth_api/plugins/oauth/
line.rs1use 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 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;