Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth2/OpenID Connect implementation #270

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ COMMON_OPTIONS := -colorize-code -stars -sort
eliomdoc_wiki = ODOC_WIKI_SUBPROJECT="$(1)" \
eliomdoc \
-$(1) \
-ppx -package pgocaml,yojson,calendar,ocsigen-toolkit.$(1) \
-ppx -package pgocaml,yojson,calendar,ocsigen-toolkit.$(1),jwt \
-intro doc/indexdoc.$(1) $(COMMON_OPTIONS) \
-i $(shell ocamlfind query wikidoc) \
-g odoc_wiki.cma \
Expand All @@ -248,7 +248,7 @@ eliomdoc_wiki = ODOC_WIKI_SUBPROJECT="$(1)" \
eliomdoc_html = ODOC_WIKI_SUBPROJECT="$(1)" \
eliomdoc \
-$(1) \
-ppx -package pgocaml,yojson,calendar,ocsigen-toolkit.$(1) \
-ppx -package pgocaml,yojson,calendar,ocsigen-toolkit.$(1),jwt \
-intro doc/indexdoc.$(1) \
$(COMMON_OPTIONS) \
-html \
Expand Down
2 changes: 1 addition & 1 deletion Makefile.options
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ SASS_TEMPORARY_PROJECT_NAME := os_temporary_project_name

# OCamlfind packages for the server
SERVER_PACKAGES := lwt.ppx js_of_ocaml.deriving.ppx calendar safepass \
ocsigen-toolkit.server magick yojson
ocsigen-toolkit.server magick yojson jwt

SERVER_DB_PACKAGES := pgocaml pgocaml.syntax macaque.syntax calendar safepass

Expand Down
1 change: 1 addition & 0 deletions opam/opam
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ depends: [
"ocsigen-toolkit" {>= "dev"}
"ppx_deriving"
"yojson"
"jwt"
]
depexts: [
[["debian"] ["imagemagick"]]
Expand Down
181 changes: 181 additions & 0 deletions src/os_connect_client.eliom
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
(* Ocsigen-start
* http://www.ocsigen.org/ocsigen-start
*
* Copyright (C) Université Paris Diderot, CNRS, INRIA, Be Sport.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, with linking exception;
* either version 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*)

open Os_oauth2_shared

exception Bad_JSON_response

exception No_such_saved_token

module type IDTOKEN =
sig
type saved_token

val saved_tokens : saved_token list ref

val cycle_duration : int

val number_of_cycle : int

val id_server_of_saved_token :
saved_token ->
Os_types.OAuth2.Server.id

val value_of_saved_token :
saved_token ->
string

val token_type_of_saved_token :
saved_token ->
string

val id_token_of_saved_token :
saved_token ->
Jwt.t

val counter_of_saved_token :
saved_token ->
int ref

val parse_json_token :
int64 ->
Yojson.Basic.json ->
saved_token

val saved_token_of_id_server_and_value :
Os_types.OAuth2.Server.id ->
string ->
saved_token

val save_token :
saved_token ->
unit

val list_tokens :
unit ->
saved_token list

val remove_saved_token :
saved_token ->
unit
end

module Basic_scope =
struct
type scope = OpenID | Firstname | Lastname | Email | Unknown

let default_scopes = [ OpenID ]

let scope_to_str = function
| OpenID -> "openid"
| Firstname -> "firstname"
| Lastname -> "lastname"
| Email -> "email"
| Unknown -> ""

let scope_of_str = function
| "openid" -> OpenID
| "firstname" -> Firstname
| "lastname" -> Lastname
| "email" -> Email
| _ -> Unknown
end

module Basic_ID_token : IDTOKEN =
struct
type saved_token =
{
id_server : Os_types.OAuth2.Server.id ;
value : string ;
token_type : string ;
counter : int ref ;
id_token : Jwt.t
}

let saved_tokens : saved_token list ref = ref []

let cycle_duration = 10

let number_of_cycle = 1

let id_server_of_saved_token t = t.id_server

let value_of_saved_token t = t.value

let token_type_of_saved_token t = t.token_type

let id_token_of_saved_token t = t.id_token

let counter_of_saved_token t = t.counter

let parse_json_token id_server t =
try
let value =
Yojson.Basic.Util.to_string (Yojson.Basic.Util.member "token" t)
in
let token_type =
Yojson.Basic.Util.to_string (Yojson.Basic.Util.member "token_type" t)
in
let id_token =
Jwt.t_of_token
(
Yojson.Basic.Util.to_string
(Yojson.Basic.Util.member "id_token" t)
)
in
{ id_server ; value ; token_type ; id_token ; counter = ref 0 }
with _ -> raise Bad_JSON_response

let save_token token =
saved_tokens := (token :: (! saved_tokens))

let saved_token_of_id_server_and_value id_server value =
let saved_tokens_tmp = ! saved_tokens in
let rec locale = function
| [] -> raise No_such_saved_token
| head::tail ->
if head.id_server = id_server && head.value = value
then head
else locale tail
in
locale saved_tokens_tmp

let list_tokens () =
(! saved_tokens)

let remove_saved_token token =
let value = value_of_saved_token token in
let id_server = id_server_of_saved_token token in
saved_tokens :=
(
List.filter
(fun (x : saved_token) ->
x.value = value && x.id_server = id_server
)
(! saved_tokens)
)
end

module Basic
: (Os_oauth2_client.CLIENT with
type scope = Basic_scope.scope and
type saved_token = Basic_ID_token.saved_token
) =
Os_oauth2_client.MakeClient (Basic_scope) (Basic_ID_token)
170 changes: 170 additions & 0 deletions src/os_connect_client.eliomi
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
(* Ocsigen-start
* http://www.ocsigen.org/ocsigen-start
*
* Copyright (C) Université Paris Diderot, CNRS, INRIA, Be Sport.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, with linking exception;
* either version 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*)

(** OpenID Connect client with default scopes ({!Basic_scope}), ID Tokens
({!Basic_ID_Token}) and client implementation ({!Basic}).
*)

(** {1 Exceptions} *)

(** Exception raised when the JSON received from the OpenID Connect server is
not well formated or if there is missing fields.
*)
exception Bad_JSON_response

(** Exception raised when the given token doesn't exist. *)
exception No_such_saved_token

(** {2 Token representation. } *)

(** Interface for ID Token used by the OpenID Connect server. *)

module type IDTOKEN = sig
(** Represent a saved token. The type is abstract to let the choice of the
implementation.
In addition to {!Os_oauth2_client.TOKEN.saved_token}, a token must contain
at least:
- the token type (for example ["bearer"]).
- the scopes list (of type {!scope}). Used to know which data the data
service must send.

- the ID token as a JSON Web Token (JWT).
*)
type saved_token

(** Represents the list of all saved tokens. *)
val saved_tokens : saved_token list ref

(** Tokens must expire after a certain amount of time. For this reason, a
timer {!Os_oauth2_shared.update_list_timer} checks all {!cycle_duration}
seconds if the token has been generated after {!cycle_duration} *
{!number_of_cycle} seconds. If it's the case, the token is removed.
*)
(** The duration of a cycle. *)
val cycle_duration : int

(** [number_of_cycle] the number of cycle. *)
val number_of_cycle : int

(** Return the OpenID Connect server ID which delivered the token. *)
val id_server_of_saved_token :
saved_token ->
Os_types.OAuth2.Server.id

(** Return the token value. *)
val value_of_saved_token :
saved_token ->
string

(** Return the token type (for example ["bearer"]. *)
val token_type_of_saved_token :
saved_token ->
string

(** Return the ID token as a JWT. *)
val id_token_of_saved_token :
saved_token ->
Jwt.t

(** Return the number of remaining cycles. *)
val counter_of_saved_token :
saved_token ->
int ref

(** [parse_json_token id_server token] parse the JSON data returned by the
token server (which has the ID [id_server] in the database) and returns
the corresponding {!save_token} OCaml type. The
Must raise {!Bad_JSON_response} if all needed information are not given.
Unrecognized JSON attributes must be ignored.
*)
val parse_json_token :
Os_types.OAuth2.Server.id ->
Yojson.Basic.json ->
saved_token

(** [saved_token_of_id_server_and_value id_server value] returns the
saved_token delivered by the server with ID [id_server] and with value
[value].
Raise an exception {!No_such_saved_token} if no token has been delivered by
[id_server] with value [value].

It implies OpenID Connect servers delivers unique token values, which is
logical for security.
*)
val saved_token_of_id_server_and_value :
Os_types.OAuth2.Server.id ->
string ->
saved_token

(** [save_token token] saves a new token. *)
val save_token :
saved_token ->
unit

(** Return all saved tokens as a list. *)
val list_tokens :
unit ->
saved_token list

(** [remove_saved_token token] removes [token] (used for example when [token]
is expired.
*)
val remove_saved_token :
saved_token ->
unit
end

(** {3 Basic modules for scopes, tokens and client. } *)

(** Basic scope for OpenID Connect. *)

module Basic_scope : sig
(** Available scopes. When doing a request, [OpenID] is automatically
set.
*)
type scope =
| OpenID (** Mandatory in each requests (due to RFC).*)
| Firstname (** Get access to the first name *)
| Lastname (** Get access to the last name *)
| Email (** Get access to the email *)
| Unknown (** Used when an unknown scope is given. *)

(** Default scopes is set to {{!scope}OpenID} (due to RFC). *)
val default_scopes : scope list

(** Get a string representation of the scope. {{!scope}Unknown} string
representation is the empty string.
*)
val scope_to_str : scope -> string

(** Converts a string scope to {!scope} type. *)
val scope_of_str : string -> scope
end

(** Basic ID token implementation. *)

module Basic_ID_token : IDTOKEN

(** Basic OpenID Connect client implementation using {!Basic_scope} and
{!Basic_ID_token}.
*)
module Basic : (Os_oauth2_client.CLIENT with
type scope = Basic_scope.scope and
type saved_token = Basic_ID_token.saved_token)
Loading