Skip to content

Commit

Permalink
feat(binding-coap): add ACE-OAuth support
Browse files Browse the repository at this point in the history
  • Loading branch information
JKRhb committed Aug 18, 2022
1 parent 4e9c065 commit bb47c03
Show file tree
Hide file tree
Showing 9 changed files with 455 additions and 4 deletions.
3 changes: 3 additions & 0 deletions lib/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
/// for protocol bindings, and the `Servient` class which provides the WoT
/// runtime used for consuming, exposing, and discovering Things.
export 'package:dcaf/dcaf.dart';

export 'src/core/codecs/content_codec.dart';
export 'src/core/content_serdes.dart';
export 'src/core/credentials/ace_credentials.dart';
export 'src/core/credentials/apikey_credentials.dart';
export 'src/core/credentials/basic_credentials.dart';
export 'src/core/credentials/bearer_credentials.dart';
Expand Down
177 changes: 173 additions & 4 deletions lib/src/binding_coap/coap_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import 'dart:async';

import 'package:coap/coap.dart' as coap;
import 'package:coap/config/coap_config_default.dart';
import 'package:dcaf/dcaf.dart';
import 'package:typed_data/typed_buffers.dart';

import '../core/content.dart';
import '../core/credentials/ace_credentials.dart';
import '../core/credentials/psk_credentials.dart';
import '../core/discovery/core_link_format.dart';
import '../core/protocol_interfaces/protocol_client.dart';
Expand Down Expand Up @@ -165,14 +167,181 @@ class CoapClient extends ProtocolClient {
block2Size: block2Size,
);

final response = await coapClient.send(
request,
onMulticastResponse: multicastResponseHandler,
);
final creationHint = await _obtainAceCreationHintFromForm(form);
final aceCredentialsCallback =
_clientSecurityProvider?.aceCredentialsCallback;

final coap.CoapResponse response;

if (aceCredentialsCallback != null && creationHint != null) {
response = await _sendAceOauthRequest(
request,
creationHint,
aceCredentialsCallback,
uri,
form,
);
} else {
response = await coapClient.send(
request,
onMulticastResponse: multicastResponseHandler,
);
}

coapClient.close();
return response.content;
}

Future<AuthServerRequestCreationHint?> _obtainCreationHintFromResourceServer(
Form form,
) async {
final requestMethod =
(CoapRequestMethod.fromForm(form) ?? CoapRequestMethod.get).code;

final creationHintUri = form.resolvedHref.replace(scheme: 'coap');

final request = await _createRequest(
requestMethod,
creationHintUri,
format: form.format,
accept: form.accept,
);

final coapClient = coap.CoapClient(
creationHintUri,
_InternalCoapConfig(_coapConfig ?? CoapConfig(), form),
);

final response = await coapClient.send(request);
coapClient.close();

return response.creationHint;
}

/// Obtains an ACE creation hint serialized as a [List] of [int] from a
/// [Form].
///
/// Returns `null` if no `ACESecurityScheme` is defined.
Future<AuthServerRequestCreationHint?> _obtainAceCreationHintFromForm(
Form? form,
) async {
if (form == null) {
return null;
}

final aceSecuritySchemes = form.aceSecuritySchemes;

if (aceSecuritySchemes.isEmpty) {
return null;
}

final aceSecurityScheme = aceSecuritySchemes.first;

AuthServerRequestCreationHint? creationHint;

if (aceSecurityScheme.cnonce ?? false) {
creationHint = await _obtainCreationHintFromResourceServer(form);
}

final textScopes = aceSecurityScheme.scopes?.join(' ');
// TODO: Do the scopes defined for a form need to be considered here as
// well?
TextScope? scope;
if (textScopes != null) {
scope = TextScope(textScopes);
}

return AuthServerRequestCreationHint(
authorizationServer:
aceSecurityScheme.as ?? creationHint?.authorizationServer,
scope: scope ?? creationHint?.scope,
audience: aceSecurityScheme.audience ?? creationHint?.audience,
clientNonce: creationHint?.clientNonce,
);
}

Future<coap.CoapResponse> _sendAceOauthRequest(
coap.CoapRequest request,
AuthServerRequestCreationHint? creationHint,
AceSecurityCallback aceCredentialsCallback,
Uri uri,
Form? form, [
AceCredentials? invalidAceCredentials,
]) async {
final aceCredentials = await aceCredentialsCallback(
uri,
form,
creationHint,
invalidAceCredentials,
);

if (aceCredentials == null) {
throw CoapBindingException('Missing ACE-OAuth Credentials');
}

final pskCredentials = aceCredentials.accessToken.pskCredentials;

final client = coap.CoapClient(
request.uri.replace(scheme: 'coaps'),
coap.CoapConfigTinydtls(),
pskCredentialsCallback: (identityHint) => pskCredentials,
);

final response = await client.send(request);
client.close();

return _handleResponse(
request,
response,
uri,
form,
aceCredentialsCallback,
invalidAceCredentials: aceCredentials,
);
}

Future<coap.CoapResponse> _handleResponse(
coap.CoapRequest request,
coap.CoapResponse response,
Uri uri,
Form? form,
AceSecurityCallback aceCredentialsCallback, {
AceCredentials? invalidAceCredentials,
}) async {
if (response.isSuccess) {
return response;
}

final errorString = '${response.code}. Payload: ${response.payloadString}';

if (response.code.isServerError) {
throw CoapBindingException(
'Server error: $errorString',
);
}

final aceCreationHint = response.creationHint;

if (aceCreationHint != null) {
if (invalidAceCredentials != null ||
form == null ||
form.usesAutoScheme) {
return _sendAceOauthRequest(
request,
aceCreationHint,
aceCredentialsCallback,
uri,
form,
invalidAceCredentials,
);
}
}

throw CoapBindingException(
'Client error: $errorString',
);
}

@override
Future<Content> readResource(Form form) async {
return _sendRequestFromForm(form, OperationType.readproperty);
Expand Down
77 changes: 77 additions & 0 deletions lib/src/binding_coap/coap_extensions.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import 'dart:io';
import 'dart:typed_data';

import 'package:cbor/cbor.dart';
import 'package:coap/coap.dart';
import 'package:dcaf/dcaf.dart';

import '../core/content.dart';
import '../definitions/form.dart';
import '../definitions/operation_type.dart';
import '../definitions/security/ace_security_scheme.dart';
import '../definitions/security/auto_security_scheme.dart';
import '../definitions/security/psk_security_scheme.dart';
import 'coap_binding_exception.dart';
import 'coap_definitions.dart';

const _validBlockwiseValues = [16, 32, 64, 128, 256, 512, 1024];
Expand All @@ -25,6 +31,10 @@ extension CoapFormExtension on Form {
bool get usesPskScheme =>
securityDefinitions.whereType<PskSecurityScheme>().isNotEmpty;

/// Determines if this [Form] supports the [AutoSecurityScheme].
bool get usesAutoScheme =>
securityDefinitions.whereType<AutoSecurityScheme>().isNotEmpty;

/// Get the [CoapSubprotocol] for this [Form], if one is set.
CoapSubprotocol? get coapSubprotocol {
if (subprotocol == coapPrefixMapping.expandCurieString('observe')) {
Expand Down Expand Up @@ -77,6 +87,10 @@ extension CoapFormExtension on Form {

/// Indicates the Block1 size preferred by a server.
int? get block1Size => _determineBlockSize('block1SZX');

/// Gets a list of all defined [AceSecurityScheme]s for this form.
List<AceSecurityScheme> get aceSecuritySchemes =>
securityDefinitions.whereType<AceSecurityScheme>().toList();
}

/// Extension for determining the corresponding [CoapRequestMethod] and
Expand Down Expand Up @@ -138,4 +152,67 @@ extension ResponseExtension on CoapResponse {
Content get content {
return Content(_contentType, _payloadStream);
}

/// Validates the payload and returns a serialized ACE creation hint if
/// successful.
AuthServerRequestCreationHint? get creationHint {
const unauthorizedAceCodes = [
// TODO: Should other response codes be included as well?
CoapCode.unauthorized,
CoapCode.methodNotAllowed,
CoapCode.forbidden,
];

final responsePayload = payload;

if (responsePayload != null &&
contentFormat == CoapMediaType.applicationAceCbor &&
unauthorizedAceCodes.contains(contentFormat)) {
return AuthServerRequestCreationHint.fromSerialized(
responsePayload.toList(),
);
}
return null;
}
}

/// Extension for conveniently retrieving [PskCredentials] from an
/// [AccessTokenResponse].
extension PskExtension on AccessTokenResponse {
void _checkAceProfile() {
final aceProfile = this.aceProfile;
if (aceProfile != null && aceProfile != AceProfile.coapDtls) {
throw CoapBindingException(
'ACE-OAuth Profile $aceProfile is not supported.',
);
}
}

/// Obtains [PskCredentials] for DTLS from this [AccessTokenResponse].
///
/// Throws a [CoapBindingException] if the deserialization should fail or the
/// wrong format has been provided.
PskCredentials get pskCredentials {
_checkAceProfile();
final identity = Uint8List.fromList(accessToken);
final cnf = this.cnf;
if (cnf is! PlainCoseKey) {
throw CoapBindingException(
'Proof of Possession Key for establishing a DTLS connection must be '
'symmetric',
);
}
final key = cnf.key.parameters[-1];
final Uint8List preSharedKey;
if (key is CborBytes) {
preSharedKey = Uint8List.fromList(key.bytes);
} else {
throw CoapBindingException(
'Proof of Possession Key for establishing a DTLS connection must be '
'bytes',
);
}

return PskCredentials(identity: identity, preSharedKey: preSharedKey);
}
}
20 changes: 20 additions & 0 deletions lib/src/core/credentials/ace_credentials.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2022 The NAMIB Project Developers. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// SPDX-License-Identifier: BSD-3-Clause

import 'package:dcaf/dcaf.dart';

import '../../definitions/security/ace_security_scheme.dart';
import 'credentials.dart';

/// [Credentials] used for the [AceSecurityScheme].
class AceCredentials extends Credentials {
/// Constructor.
AceCredentials(this.accessToken) : super('ace:ACESecurityScheme');

/// The access token associated with these [AceCredentials] in serialized
/// form.
final AccessTokenResponse accessToken;
}
22 changes: 22 additions & 0 deletions lib/src/core/security_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

import 'dart:typed_data';

import 'package:dcaf/dcaf.dart';

import '../definitions/form.dart';
import 'credentials/ace_credentials.dart';
import 'credentials/apikey_credentials.dart';
import 'credentials/basic_credentials.dart';
import 'credentials/bearer_credentials.dart';
Expand All @@ -31,6 +34,21 @@ typedef ClientPskCallback = PskCredentials? Function(
Uint8List? identityHint,
);

/// Function signature for an asynchronous callback for providing client
/// [AceCredentials] at runtime, based on an optional [creationHint]
/// given by the Resource Server. This creation hint has to be parsed by the
/// library user.
///
/// If a request with an access token has failed before, leading to an
/// "Unauthorized" response, the [invalidAceCredentials] from the previous
/// request are returned as an additional parameter.
typedef AceSecurityCallback = Future<AceCredentials?> Function(
Uri uri,
Form? form,
AuthServerRequestCreationHint? creationHint,
AceCredentials? invalidAceCredentials,
);

/// Function signature for a synchronous callback for providing client
/// [Credentials] at runtime.
///
Expand Down Expand Up @@ -67,6 +85,7 @@ class ClientSecurityProvider {
this.digestCredentialsCallback,
this.apikeyCredentialsCallback,
this.oauth2CredentialsCallback,
this.aceCredentialsCallback,
});

/// Asychronous callback for [ApiKeyCredentials].
Expand All @@ -91,6 +110,9 @@ class ClientSecurityProvider {
/// Asychronous callback for [OAuth2Credentials].
final AsyncClientSecurityCallback<OAuth2Credentials>?
oauth2CredentialsCallback;

/// Asynchronous callback for [AceCredentials].
final AceSecurityCallback? aceCredentialsCallback;
}

/// Function signature for a synchronous callback retrieving server
Expand Down
Loading

0 comments on commit bb47c03

Please sign in to comment.