Skip to content

Commit 7edf533

Browse files
authored
feat(dart): add logto core method for dart sdk (#10)
1 parent 9d9846a commit 7edf533

18 files changed

+515
-102
lines changed

example/lib/main.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'package:http/http.dart' as http;
22
import 'package:flutter/material.dart';
3-
import 'package:logto_dart_sdk/logto_core.dart';
3+
import 'package:logto_dart_sdk/logto_core.dart' as logto_core;
44

55
void main() {
66
runApp(const MyApp());
@@ -43,8 +43,9 @@ class _MyHomePageState extends State<MyHomePage> {
4343
}
4444

4545
void _init() async {
46-
LogtoCore.fetchOidcConfig(
47-
"https://logto.dev/oidc/.well-known/openid-configuration", client)
46+
logto_core
47+
.fetchOidcConfig(
48+
client, "https://logto.dev/oidc/.well-known/openid-configuration")
4849
.then((value) => {
4950
setState(() {
5051
content = value.toJson().toString();

example/test/widget_test.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'package:nock/nock.dart';
33

44
import 'package:example/main.dart';
55

6-
import '../../test/mocks/oidc_config.dart';
6+
import '../../test/mocks/responses.dart';
77

88
void main() {
99
setUpAll(nock.init);
@@ -13,12 +13,10 @@ void main() {
1313
});
1414

1515
testWidgets('Logto Dart Demo App', (WidgetTester tester) async {
16-
final interceptor =
17-
nock("https://logto.dev").get("/oidc/.well-known/openid-configuration")
18-
..reply(
19-
200,
20-
mockOidcConfigResponse,
21-
);
16+
final interceptor = nock("https://logto.dev")
17+
.get("/oidc/.well-known/openid-configuration")
18+
..reply(200, mockOidcConfigResponse,
19+
headers: {'Content-Type': 'application/json'});
2220

2321
// Build our app and trigger a frame.
2422
await tester.pumpWidget(const MyApp());

lib/logto_core.dart

Lines changed: 127 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,142 @@
1-
import 'dart:convert';
2-
31
import 'package:http/http.dart' as http;
2+
import 'package:logto_dart_sdk/src/interfaces/logto_user_info_response.dart';
43

5-
import '/src/exceptions/http_request_exceptions.dart';
6-
import '/src/interfaces/oidc_provider_config.dart';
4+
import '/src/interfaces/logto_interfaces.dart';
75
import '/src/utilities/utils.dart';
6+
import '/src/utilities/http_utils.dart';
7+
import '/src/utilities/constants.dart';
8+
9+
const String _codeChallengeMethod = 'S256';
10+
const String _responseType = 'code';
11+
const String _prompt = 'consent';
12+
const String _requestContentType = 'application/x-www-form-urlencoded';
13+
14+
Future<OidcProviderConfig> fetchOidcConfig(
15+
http.Client httpClient, String endpoint) async {
16+
final response = await httpClient.get(Uri.parse(endpoint));
17+
18+
var body = httpResponseHandler(response);
19+
20+
return OidcProviderConfig.fromJson(body);
21+
}
822

9-
class LogtoCore {
10-
static const String codeChallengeMethod = 'S256';
11-
static const String responseType = 'code';
12-
static const String prompt = 'consent';
23+
Future<LogtoCodeTokenResponse> fetchTokenByAuthorizationCode(
24+
{required http.Client httpClient,
25+
required String tokenEndPoint,
26+
required String code,
27+
required String codeVerifier,
28+
required String clientId,
29+
required String redirectUri,
30+
String? resource}) async {
31+
Map<String, dynamic> payload = {
32+
'grant_type': authorizationCodeGrantType,
33+
'code': code,
34+
'code_verifier': codeVerifier,
35+
'client_id': clientId,
36+
'redirect_uri': redirectUri,
37+
};
1338

14-
static Future<OidcProviderConfig> fetchOidcConfig(
15-
String endpoint, http.Client httpClient) async {
16-
final response = await httpClient.get(Uri.parse(endpoint));
39+
if (resource != null && resource.isNotEmpty) {
40+
payload.addAll({'resource': resource});
41+
}
42+
43+
final response = await httpClient.post(Uri.parse(tokenEndPoint),
44+
headers: {'Content-Type': _requestContentType}, body: payload);
45+
46+
var body = httpResponseHandler(response);
1747

18-
var body = jsonDecode(response.body);
48+
return LogtoCodeTokenResponse.fromJson(body);
49+
}
1950

20-
if (response.statusCode < 200 || response.statusCode >= 300) {
21-
throw HttpRequestException(statusCode: response.statusCode, body: body);
22-
}
51+
Future<LogtoRefreshTokenResponse> fetchTokenByRefreshToken({
52+
required http.Client httpClient,
53+
required String tokenEndPoint,
54+
required String clientId,
55+
required String refreshToken,
56+
String? resource,
57+
List<String>? scopes,
58+
}) async {
59+
Map<String, dynamic> payload = {
60+
'grant_type': refreshTokenGrantType,
61+
'client_id': clientId,
62+
'refresh_token': refreshToken,
63+
};
2364

24-
return OidcProviderConfig.fromJson(body);
65+
if (resource != null && resource.isNotEmpty) {
66+
payload.addAll({'resource': resource});
2567
}
2668

27-
static Uri generateSignInUri(
28-
{required String authorizationEndpoint,
29-
required clientId,
30-
required Uri redirectUri,
31-
required String codeChallenge,
32-
required String state,
33-
List<String> scopes = const [],
34-
List<String>? resources,
35-
String prompt = LogtoCore.prompt}) {
36-
var signInUri = Uri.parse(authorizationEndpoint);
37-
38-
Map<String, dynamic> queryParameters = {
39-
'client_id': clientId,
40-
'redirect_uri': redirectUri.toString(),
41-
'code_challenge': codeChallenge,
42-
'code_challenge_method': LogtoCore.codeChallengeMethod,
43-
'state': state,
44-
'scope': withReservedScopes(scopes).join(' '),
45-
'response_type': LogtoCore.responseType,
46-
'prompt': prompt,
47-
};
48-
49-
if (resources != null && resources.isNotEmpty) {
50-
queryParameters.addAll({'resources': resources});
51-
}
52-
53-
return addQueryParameters(signInUri, queryParameters);
69+
if (scopes != null && scopes.isNotEmpty) {
70+
payload.addAll({'scope': scopes.join(' ')});
5471
}
5572

56-
static Uri generateSignOutUri(
57-
{required String endSessionEndpoint,
58-
required String idToken,
59-
required Uri postLogoutRedirectUri}) {
60-
var signOutUri = Uri.parse(endSessionEndpoint);
73+
final response = await httpClient.post(Uri.parse(tokenEndPoint),
74+
headers: {'Content-Type': _requestContentType}, body: payload);
75+
76+
var body = httpResponseHandler(response);
77+
78+
return LogtoRefreshTokenResponse.fromJson(body);
79+
}
80+
81+
Future<LogtoUserInfoResponse> fetchUserInfo(
82+
{required http.Client httpClient,
83+
required String userInfoEndpoint,
84+
required String accessToken}) async {
85+
final response = await httpClient.post(Uri.parse(userInfoEndpoint),
86+
headers: {'Authorization': 'Bearer $accessToken'});
87+
88+
var body = httpResponseHandler(response);
89+
90+
return LogtoUserInfoResponse.fromJson(body);
91+
}
92+
93+
Future<void> revoke({
94+
required http.Client httpClient,
95+
required String revocationEndpoint,
96+
required String clientId,
97+
required String token,
98+
}) =>
99+
httpClient.post(Uri.parse(revocationEndpoint),
100+
headers: {'Content-Type': _requestContentType},
101+
body: {'client_id': clientId, 'token': token});
61102

62-
return addQueryParameters(signOutUri, {
63-
'id_token_hint': idToken,
64-
'post_logout_redirect_uri': postLogoutRedirectUri.toString()
65-
});
103+
Uri generateSignInUri(
104+
{required String authorizationEndpoint,
105+
required clientId,
106+
required Uri redirectUri,
107+
required String codeChallenge,
108+
required String state,
109+
List<String> scopes = const [],
110+
List<String>? resources,
111+
String prompt = _prompt}) {
112+
var signInUri = Uri.parse(authorizationEndpoint);
113+
114+
Map<String, dynamic> queryParameters = {
115+
'client_id': clientId,
116+
'redirect_uri': redirectUri.toString(),
117+
'code_challenge': codeChallenge,
118+
'code_challenge_method': _codeChallengeMethod,
119+
'state': state,
120+
'scope': withReservedScopes(scopes).join(' '),
121+
'response_type': _responseType,
122+
'prompt': prompt,
123+
};
124+
125+
if (resources != null && resources.isNotEmpty) {
126+
queryParameters.addAll({'resources': resources});
66127
}
128+
129+
return addQueryParameters(signInUri, queryParameters);
130+
}
131+
132+
Uri generateSignOutUri(
133+
{required String endSessionEndpoint,
134+
required String idToken,
135+
required Uri postLogoutRedirectUri}) {
136+
var signOutUri = Uri.parse(endSessionEndpoint);
137+
138+
return addQueryParameters(signOutUri, {
139+
'id_token_hint': idToken,
140+
'post_logout_redirect_uri': postLogoutRedirectUri.toString()
141+
});
67142
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
3+
part 'logto_code_token_response.g.dart';
4+
5+
@JsonSerializable()
6+
class LogtoCodeTokenResponse {
7+
@JsonKey(name: 'access_token', required: true, disallowNullValue: true)
8+
final String accessToken;
9+
@JsonKey(name: 'refresh_token')
10+
final String? refreshToken;
11+
@JsonKey(name: 'id_token', required: true, disallowNullValue: true)
12+
final String idToken;
13+
@JsonKey(name: 'scope', required: true, disallowNullValue: true)
14+
final String scope;
15+
@JsonKey(name: 'expires_in', required: true, disallowNullValue: true)
16+
final int expiresIn;
17+
18+
LogtoCodeTokenResponse({
19+
required this.accessToken,
20+
required this.idToken,
21+
required this.scope,
22+
required this.expiresIn,
23+
this.refreshToken,
24+
});
25+
26+
factory LogtoCodeTokenResponse.fromJson(Map<String, dynamic> json) =>
27+
_$LogtoCodeTokenResponseFromJson(json);
28+
29+
Map<String, dynamic> toJson() => _$LogtoCodeTokenResponseToJson(this);
30+
}

lib/src/interfaces/logto_code_token_response.g.dart

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export 'logto_config.dart';
22
export 'oidc_provider_config.dart';
3+
export 'logto_code_token_response.dart';
4+
export 'logto_refresh_token_response.dart';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
3+
part 'logto_refresh_token_response.g.dart';
4+
5+
@JsonSerializable()
6+
class LogtoRefreshTokenResponse {
7+
@JsonKey(name: 'access_token', required: true, disallowNullValue: true)
8+
final String accessToken;
9+
@JsonKey(name: 'refresh_token', required: true, disallowNullValue: true)
10+
final String refreshToken;
11+
@JsonKey(name: 'id_token')
12+
final String? idToken;
13+
@JsonKey(name: 'scope', required: true, disallowNullValue: true)
14+
final String scope;
15+
@JsonKey(name: 'expires_in', required: true, disallowNullValue: true)
16+
final int expiresIn;
17+
18+
LogtoRefreshTokenResponse(
19+
{required this.accessToken,
20+
required this.refreshToken,
21+
this.idToken,
22+
required this.expiresIn,
23+
required this.scope});
24+
25+
factory LogtoRefreshTokenResponse.fromJson(Map<String, dynamic> json) =>
26+
_$LogtoRefreshTokenResponseFromJson(json);
27+
28+
Map<String, dynamic> toJson() => _$LogtoRefreshTokenResponseToJson(this);
29+
}

lib/src/interfaces/logto_refresh_token_response.g.dart

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)